|
|
@@ -7,12 +7,16 @@
|
|
|
<text class="device-name">{{ deviceInfo.name }}</text>
|
|
|
<view
|
|
|
class="status-tag"
|
|
|
- :class="deviceInfo.status === 'online' ? 'status-online' : 'status-offline'"
|
|
|
+ :class="deviceInfo.status === 1 ? 'status-online' : 'status-offline'"
|
|
|
>
|
|
|
- <view class="status-dot" :class="{'offline-dot': deviceInfo.status === 'offline'}"></view>
|
|
|
- {{ deviceInfo.status === 'online' ? '在线' : '离线' }}
|
|
|
+ <view class="status-dot" :class="{'offline-dot': deviceInfo.status === 1}"></view>
|
|
|
+ {{ deviceInfo.status === 1 ? '在线' : '离线' }}
|
|
|
</view>
|
|
|
</view>
|
|
|
+
|
|
|
+ <view class="refresh-btn" :class="{'refreshing': isRefreshing}" @tap="refreshData">
|
|
|
+ <image src="/static/icons/refresh_icon.png" mode="aspectFit" style="width: 22px; height: 22px;"></image>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
|
|
|
<view class="device-meta-row">
|
|
|
@@ -37,30 +41,38 @@
|
|
|
<image src="/static/icons/clock_icon.png" mode="aspectFit" style="width: 36rpx; height: 36rpx;"></image>
|
|
|
</view>
|
|
|
<text class="meta-label">最近更新:</text>
|
|
|
- <text class="meta-value">{{ deviceInfo.lastUpdate }}</text>
|
|
|
+ <text class="meta-value">{{ formatDate(deviceInfo.lastUpdate) }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
<!-- 视频预览区域 -->
|
|
|
<view class="video-section">
|
|
|
- <view class="video-container">
|
|
|
+ <view class="video-container" :class="{'fullscreen-mode': isFullscreen}">
|
|
|
<image v-if="!isPlaying" src="/static/images/video-placeholder.jpg" mode="aspectFill" class="video-placeholder"></image>
|
|
|
- <!-- <video
|
|
|
- v-else
|
|
|
- id="videoPlayer"
|
|
|
- :src="deviceInfo.streamUrl"
|
|
|
+
|
|
|
+ <!-- 使用跨平台视频播放组件 -->
|
|
|
+ <!-- #ifdef H5 -->
|
|
|
+ <view v-if="isPlaying" class="h5-video-wrapper">
|
|
|
+ <Jessibuca ref="jessibucaRef" :videoUrl="getH5StreamUrl" :hasAudio="true" @error="onVideoError" />
|
|
|
+ </view>
|
|
|
+ <!-- #endif -->
|
|
|
+
|
|
|
+ <!-- #ifdef MP-WEIXIN -->
|
|
|
+ <live-player
|
|
|
+ v-if="isPlaying"
|
|
|
+ id="videoPlayer"
|
|
|
+ :src="getMiniProgramStreamUrl"
|
|
|
+ mode="live"
|
|
|
+ :autoplay="true"
|
|
|
+ :muted="isMuted"
|
|
|
+ object-fit="contain"
|
|
|
+ @statechange="onStateChange"
|
|
|
+ @error="onVideoError"
|
|
|
+ @fullscreenchange="onFullscreenChange"
|
|
|
class="video-player"
|
|
|
- object-fit="cover"
|
|
|
- autoplay
|
|
|
- :controls="false"
|
|
|
- :show-center-play-btn="false"
|
|
|
- :show-fullscreen-btn="false"
|
|
|
- :show-play-btn="false"
|
|
|
- :enable-progress-gesture="false"
|
|
|
- @error="handleVideoError"
|
|
|
- ></video> -->
|
|
|
- <iframe v-else src="http://121.4.16.100:28080/#/play/wasm/ws%3A%2F%2F121.4.16.100%3A6080%2Frtp%2F34020000001110000001_34020000001320000012.live.flv"></iframe>
|
|
|
+ ></live-player>
|
|
|
+ <!-- #endif -->
|
|
|
|
|
|
<!-- 视频控制层 -->
|
|
|
<view class="video-controls">
|
|
|
@@ -94,39 +106,39 @@
|
|
|
</view>
|
|
|
|
|
|
<!-- 云台控制区域 -->
|
|
|
- <view class="ptz-section">
|
|
|
+ <!-- <view class="ptz-section">
|
|
|
<view class="section-title">云台控制</view>
|
|
|
|
|
|
<view class="ptz-container">
|
|
|
- <!-- 圆形云台控制 -->
|
|
|
+ // 圆形云台控制
|
|
|
<view class="ptz-circle-container">
|
|
|
- <!-- 上箭头 -->
|
|
|
+ 上箭头
|
|
|
<view class="ptz-arrow ptz-up" @touchstart="controlPTZ('up', true)" @touchend="controlPTZ('up', false)">
|
|
|
<image src="/static/icons/arrow_up_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 左箭头 -->
|
|
|
+ // 左箭头
|
|
|
<view class="ptz-arrow ptz-left" @touchstart="controlPTZ('left', true)" @touchend="controlPTZ('left', false)">
|
|
|
<image src="/static/icons/arrow_left_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 中心拍照按钮 -->
|
|
|
+ 中心拍照按钮
|
|
|
<view class="ptz-center" @click="takeScreenshot">
|
|
|
<image src="/static/icons/camera_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 右箭头 -->
|
|
|
+ 右箭头
|
|
|
<view class="ptz-arrow ptz-right" @touchstart="controlPTZ('right', true)" @touchend="controlPTZ('right', false)">
|
|
|
<image src="/static/icons/arrow_right_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 下箭头 -->
|
|
|
+ 下箭头
|
|
|
<view class="ptz-arrow ptz-down" @touchstart="controlPTZ('down', true)" @touchend="controlPTZ('down', false)">
|
|
|
<image src="/static/icons/arrow_down_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 底部操作按钮 -->
|
|
|
+ 底部操作按钮
|
|
|
<view class="ptz-bottom-controls">
|
|
|
<view class="ptz-bottom-button" @touchstart="controlZoom('out', true)" @touchend="controlZoom('out', false)">
|
|
|
<image src="/static/icons/zoom01_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
|
|
|
@@ -141,10 +153,10 @@
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
- </view>
|
|
|
+ </view> -->
|
|
|
|
|
|
<!-- 快捷功能按钮 -->
|
|
|
- <view class="quick-actions">
|
|
|
+ <!-- <view class="quick-actions">
|
|
|
<view class="action-button" @click="toggleVoiceIntercom">
|
|
|
<view class="action-icon">
|
|
|
<image src="/static/icons/Voice_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
|
|
|
@@ -166,7 +178,7 @@
|
|
|
</view>
|
|
|
<text class="action-text">视频回看</text>
|
|
|
</view>
|
|
|
- </view>
|
|
|
+ </view> -->
|
|
|
|
|
|
<!-- 告警信息列表 -->
|
|
|
<view class="alerts-section">
|
|
|
@@ -177,29 +189,29 @@
|
|
|
|
|
|
<view class="alerts-list" v-if="getUnhandledAlerts.length > 0">
|
|
|
<view
|
|
|
- v-for="(item, index) in getUnhandledAlerts"
|
|
|
+ v-for="(item, index) in getUnhandledAlerts.slice(0, 3)"
|
|
|
:key="index"
|
|
|
class="alert-item"
|
|
|
:class="{
|
|
|
- 'alert-urgent': item.level === 'high',
|
|
|
- 'alert-warning': item.level === 'medium',
|
|
|
- 'alert-info': item.level === 'low'
|
|
|
+ 'alert-urgent': item.level === 3,
|
|
|
+ 'alert-warning': item.level === 2,
|
|
|
+ 'alert-info': item.level === 1
|
|
|
}"
|
|
|
>
|
|
|
<view class="alert-item-icon">
|
|
|
- <image v-if="item.level === 'high'" src="/static/icons/warning_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
|
|
|
- <image v-else-if="item.level === 'medium'" src="/static/icons/info_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
|
|
|
+ <image v-if="item.level === 3" src="/static/icons/warning_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
|
|
|
+ <image v-else-if="item.level === 2" src="/static/icons/info_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
|
|
|
<image v-else src="/static/icons/success_icon.png" mode="aspectFit" style="width: 24px; height: 24px;"></image>
|
|
|
</view>
|
|
|
|
|
|
<view class="alert-item-info">
|
|
|
<text class="alert-item-type">{{ item.type }}</text>
|
|
|
<text class="alert-item-level">
|
|
|
- {{ item.level === 'high' ? '紧急' : item.level === 'medium' ? '警告' : '提示' }}
|
|
|
+ {{ item.level === 3 ? '紧急' : item.level === 2 ? '警告' : '提示' }}
|
|
|
</text>
|
|
|
</view>
|
|
|
|
|
|
- <view class="alert-item-time">{{ item.time }}</view>
|
|
|
+ <view class="alert-item-time">{{ formatSmartTime(item.time) }}</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
@@ -211,17 +223,34 @@
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
+ import { getDeviceCollectorDetail } from "@/api/services/device.js";
|
|
|
+ import { formatSmartTime, formatDate, getFormattedTime} from '@/utils/dateUtils'
|
|
|
+ import { isPlayableInMiniProgram, buildPlatformStreamUrls } from '@/utils/media-utils'
|
|
|
+ import config from '@/config/config'
|
|
|
+ // 导入Jessibuca组件
|
|
|
+ // #ifdef H5
|
|
|
+ import Jessibuca from '@/components/common/jessibuca.vue'
|
|
|
+ // #endif
|
|
|
+
|
|
|
export default {
|
|
|
+ components: {
|
|
|
+ // #ifdef H5
|
|
|
+ Jessibuca
|
|
|
+ // #endif
|
|
|
+ },
|
|
|
data() {
|
|
|
return {
|
|
|
deviceInfo: {
|
|
|
- deviceId: 'DEV1001',
|
|
|
- name: '监控设备-1',
|
|
|
- status: 'online',
|
|
|
- location: '西区B2地块',
|
|
|
- lastUpdate: '5分钟前',
|
|
|
- streamUrl: 'https://demo-rtsp-server-2h4n.onrender.com/stream.mp4',
|
|
|
- alertCount: 3
|
|
|
+ deviceId: '',
|
|
|
+ name: '设备加载中...',
|
|
|
+ status: '',
|
|
|
+ location: '正在获取位置...',
|
|
|
+ lastUpdate: '',
|
|
|
+ deviceType: 'weather' ,// 默认类型,会根据API返回更新
|
|
|
+ deviceTypeId:null,
|
|
|
+ streamUrl: '',
|
|
|
+ originalStreamUrl: 'ws://121.4.16.100:6080/rtp/34020000001110000001_34020000001320000012.live.flv',
|
|
|
+
|
|
|
},
|
|
|
isPlaying: false,
|
|
|
isMuted: false,
|
|
|
@@ -239,27 +268,48 @@ export default {
|
|
|
{ id: 4, startTime: '昨天 14:20', duration: '00:10:05', url: '' }
|
|
|
],
|
|
|
// 模拟告警数据
|
|
|
- alertHistory: [
|
|
|
- { id: 1, time: '今天 13:05', type: '移动侦测', status: '未处理', level: 'high' },
|
|
|
- { id: 2, time: '今天 09:30', type: '信号异常', status: '未处理', level: 'medium' },
|
|
|
- { id: 3, time: '昨天 15:45', type: '设备状态', status: '已处理', level: 'low' },
|
|
|
- { id: 4, time: '昨天 10:20', type: '遮挡告警', status: '未处理', level: 'low' }
|
|
|
- ],
|
|
|
- videoContext: null
|
|
|
+ alertHistory: [],
|
|
|
+ livePlayerContext: null, // 小程序视频上下文
|
|
|
}
|
|
|
},
|
|
|
|
|
|
computed: {
|
|
|
// 获取所有未处理的告警
|
|
|
getUnhandledAlerts() {
|
|
|
- return this.alertHistory.filter(alert => alert.status === '未处理');
|
|
|
- }
|
|
|
+ return this.alertHistory.filter(alert => alert.status === 0);
|
|
|
+ },
|
|
|
+ // 获取H5环境使用的流地址
|
|
|
+ getH5StreamUrl() {
|
|
|
+ // 首先尝试使用设备返回的流URL
|
|
|
+ if (this.deviceInfo.streamUrl) {
|
|
|
+ return this.deviceInfo.streamUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有设备特定的URL,则使用默认原始流
|
|
|
+ return this.deviceInfo.originalStreamUrl || config.streamServer.wsFlvServer;
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取小程序环境使用的流地址
|
|
|
+ getMiniProgramStreamUrl() {
|
|
|
+ // 获取合适的小程序流地址
|
|
|
+ const { streamServer } = config;
|
|
|
+
|
|
|
+ // 如果有设备特定的RTMP流,优先使用
|
|
|
+ if (this.deviceInfo.rtmpUrl) {
|
|
|
+ return this.deviceInfo.rtmpUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 其次尝试使用HLS流
|
|
|
+ if (this.deviceInfo.hlsUrl) {
|
|
|
+ return this.deviceInfo.hlsUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 最后使用配置的默认流
|
|
|
+ return streamServer.rtmpServer || streamServer.hlsServer;
|
|
|
+ },
|
|
|
},
|
|
|
|
|
|
onReady() {
|
|
|
- // 获取视频实例
|
|
|
- this.videoContext = uni.createVideoContext('videoPlayer')
|
|
|
-
|
|
|
// 设置页面标题
|
|
|
uni.setNavigationBarTitle({
|
|
|
title: this.deviceInfo.name
|
|
|
@@ -267,87 +317,428 @@ export default {
|
|
|
|
|
|
// 模拟更新时间
|
|
|
this.startTimeUpdate()
|
|
|
+
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ // 创建小程序视频上下文
|
|
|
+ this.livePlayerContext = uni.createLivePlayerContext('videoPlayer', this)
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef H5
|
|
|
+ // 加载Jessibuca的JS库
|
|
|
+ this.loadJessibucaScript()
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // 监听全屏状态变化
|
|
|
+ this.setupFullscreenListener()
|
|
|
+
|
|
|
+ // 添加键盘事件监听
|
|
|
+ this.setupKeyboardListener()
|
|
|
+
|
|
|
+ // 初始化流地址
|
|
|
+ this.initStreamUrl()
|
|
|
},
|
|
|
+ beforeUnmount() {
|
|
|
+ // 移除全屏状态监听
|
|
|
+ this.removeFullscreenListener()
|
|
|
+
|
|
|
+ // 移除键盘事件监听
|
|
|
+ this.removeKeyboardListener()
|
|
|
+ },
|
|
|
|
|
|
onLoad(options) {
|
|
|
- // 如果有传入设备ID,则获取设备信息
|
|
|
- if (options && options.id) {
|
|
|
- this.fetchDeviceInfo(options.id)
|
|
|
- }
|
|
|
+ uni.$once('passDeviceData', (data) => {
|
|
|
+ console.log('接收到数据', data);
|
|
|
+ // 如果有传入设备ID,则获取设备信息
|
|
|
+ if (data && data.deviceId) {
|
|
|
+ this.deviceInfo.deviceId = data.deviceId;
|
|
|
+ this.deviceInfo.location = data.fieldName;
|
|
|
+ this.deviceInfo.name = data.deviceName;
|
|
|
+ this.deviceInfo.status = data.status;
|
|
|
+ this.deviceInfo.deviceTypeId = data.deviceTypeId || '';
|
|
|
+
|
|
|
+ // 加载设备详情
|
|
|
+ this.fetchDeviceInfo();
|
|
|
+ }
|
|
|
+ });
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
+ formatDate,
|
|
|
+ formatSmartTime,
|
|
|
+ // 加载Jessibuca脚本
|
|
|
+ loadJessibucaScript() {
|
|
|
+ const script = document.createElement('script')
|
|
|
+ script.src = '/static/js/jessibuca/jessibuca.js'
|
|
|
+ script.onload = () => {
|
|
|
+ console.log('Jessibuca 脚本加载成功')
|
|
|
+ }
|
|
|
+ script.onerror = (error) => {
|
|
|
+ console.error('Jessibuca 脚本加载失败:', error)
|
|
|
+ }
|
|
|
+ document.head.appendChild(script)
|
|
|
+ },
|
|
|
// 获取设备信息
|
|
|
fetchDeviceInfo(deviceId) {
|
|
|
// 这里应该是API请求,暂时用模拟数据
|
|
|
console.log('获取设备信息:', deviceId)
|
|
|
- // 模拟异步获取数据
|
|
|
- setTimeout(() => {
|
|
|
- // 实际应该是API请求结果
|
|
|
- }, 500)
|
|
|
+ getDeviceCollectorDetail(this.deviceInfo.deviceId)
|
|
|
+ .then(res => {
|
|
|
+ if (res.data.data && res.data.code === 200) {
|
|
|
+ const detail = res.data.data;
|
|
|
+ this.deviceInfo.lastUpdate = getFormattedTime()
|
|
|
+ // 更新页面标题
|
|
|
+ uni.setNavigationBarTitle({
|
|
|
+ title: this.deviceInfo.name
|
|
|
+ });
|
|
|
+ // 更新告警信息
|
|
|
+ if (detail.alertRecordList && detail.alertRecordList.length > 0) {
|
|
|
+ this.alertHistory = detail.alertRecordList.map(alert => ({
|
|
|
+ id: alert.alertId,
|
|
|
+ time: alert.alertTime,
|
|
|
+ type: alert.alertContent,
|
|
|
+ status: alert.processStatus,
|
|
|
+ level: alert.alertLevel
|
|
|
+ }));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
},
|
|
|
|
|
|
// 播放/暂停切换
|
|
|
togglePlayState() {
|
|
|
if (!this.isPlaying) {
|
|
|
// 从未播放状态切换到播放状态
|
|
|
- this.isPlaying = true;
|
|
|
+ this.isPlaying = true
|
|
|
|
|
|
- // 短暂延迟确保视频元素已加载
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ // 小程序环境中,短暂延迟确保组件已加载
|
|
|
setTimeout(() => {
|
|
|
- if (this.videoContext) {
|
|
|
- this.videoContext.play();
|
|
|
- // 振动反馈
|
|
|
- uni.vibrateShort();
|
|
|
+ if (this.livePlayerContext) {
|
|
|
+ this.livePlayerContext.play({
|
|
|
+ success: () => {
|
|
|
+ console.log('小程序视频播放成功')
|
|
|
+ uni.vibrateShort()
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.error('小程序视频播放失败:', err)
|
|
|
+ uni.showToast({
|
|
|
+ title: '视频播放失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
}
|
|
|
- }, 300);
|
|
|
+ }, 300)
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef H5
|
|
|
+ // 浏览器环境中使用Jessibuca
|
|
|
+ setTimeout(() => {
|
|
|
+ uni.vibrateShort()
|
|
|
+ }, 300)
|
|
|
+ // #endif
|
|
|
} else {
|
|
|
// 从播放状态切换到暂停状态
|
|
|
- if (this.videoContext) {
|
|
|
- this.videoContext.pause();
|
|
|
-
|
|
|
- // 在页面上显示暂停状态
|
|
|
- uni.showToast({
|
|
|
- title: '视频已暂停',
|
|
|
- icon: 'none',
|
|
|
- duration: 1500
|
|
|
- });
|
|
|
+ // #ifdef H5
|
|
|
+ if (this.$refs.jessibucaRef) {
|
|
|
+ this.$refs.jessibucaRef.pause()
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ if (this.livePlayerContext) {
|
|
|
+ this.livePlayerContext.pause()
|
|
|
}
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ this.isPlaying = false
|
|
|
+
|
|
|
+ // 在页面上显示暂停状态
|
|
|
+ uni.showToast({
|
|
|
+ title: '视频已暂停',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 1500
|
|
|
+ })
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// 静音切换
|
|
|
toggleMute() {
|
|
|
this.isMuted = !this.isMuted
|
|
|
- if (this.videoContext) {
|
|
|
+
|
|
|
+ // #ifdef H5
|
|
|
+ if (this.$refs.jessibucaRef) {
|
|
|
if (this.isMuted) {
|
|
|
- this.videoContext.mute()
|
|
|
+ this.$refs.jessibucaRef.mute()
|
|
|
} else {
|
|
|
- this.videoContext.unmute()
|
|
|
+ this.$refs.jessibucaRef.cancelMute()
|
|
|
}
|
|
|
}
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ // 小程序环境中直接通过属性绑定控制静音
|
|
|
+ // #endif
|
|
|
},
|
|
|
|
|
|
// 全屏切换
|
|
|
toggleFullscreen() {
|
|
|
if (!this.isPlaying) {
|
|
|
// 如果视频未播放,先开始播放
|
|
|
- this.togglePlayState();
|
|
|
+ this.togglePlayState()
|
|
|
// 延迟执行全屏操作,等待视频元素加载
|
|
|
setTimeout(() => {
|
|
|
- if (this.videoContext) {
|
|
|
- this.videoContext.requestFullScreen();
|
|
|
- this.isFullscreen = true;
|
|
|
+ this.setFullscreen(true)
|
|
|
+ }, 500)
|
|
|
+ } else {
|
|
|
+ this.setFullscreen(!this.isFullscreen)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 设置全屏状态
|
|
|
+ setFullscreen(fullscreen) {
|
|
|
+ this.isFullscreen = fullscreen
|
|
|
+
|
|
|
+ // #ifdef H5
|
|
|
+ // 浏览器环境
|
|
|
+ if (this.isFullscreen) {
|
|
|
+ // 如果是移动设备,尝试使用系统全屏
|
|
|
+ const ua = navigator.userAgent.toLowerCase()
|
|
|
+ const isMobile = /mobile|android|iphone|ipad/.test(ua)
|
|
|
+
|
|
|
+ if (isMobile && this.$refs.jessibucaRef) {
|
|
|
+ // 在移动设备上使用Jessibuca的全屏API
|
|
|
+ this.$refs.jessibucaRef.fullscreenSwich()
|
|
|
+ } else {
|
|
|
+ // 适配屏幕尺寸
|
|
|
+ setTimeout(() => {
|
|
|
+ if (this.$refs.jessibucaRef) {
|
|
|
+ this.$refs.jessibucaRef.resize()
|
|
|
+ }
|
|
|
+ }, 300)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 锁定屏幕方向为横屏
|
|
|
+ if (window.screen && window.screen.orientation && window.screen.orientation.lock) {
|
|
|
+ window.screen.orientation.lock('landscape').catch(err => {
|
|
|
+ console.error('无法锁定屏幕方向:', err)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 退出全屏状态
|
|
|
+ if (this.$refs.jessibucaRef) {
|
|
|
+ // 在某些情况下需要先调用Jessibuca的退出全屏
|
|
|
+ if (this.$refs.jessibucaRef.isFullscreen()) {
|
|
|
+ this.$refs.jessibucaRef.fullscreenSwich()
|
|
|
}
|
|
|
- }, 500);
|
|
|
- } else if (this.videoContext) {
|
|
|
- if (!this.isFullscreen) {
|
|
|
- this.videoContext.requestFullScreen();
|
|
|
+
|
|
|
+ // 重置视频大小
|
|
|
+ setTimeout(() => {
|
|
|
+ if (this.$refs.jessibucaRef) {
|
|
|
+ this.$refs.jessibucaRef.resize()
|
|
|
+ }
|
|
|
+ }, 300)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解除屏幕方向锁定
|
|
|
+ if (window.screen && window.screen.orientation && window.screen.orientation.unlock) {
|
|
|
+ window.screen.orientation.unlock()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ // 小程序环境
|
|
|
+ if (this.livePlayerContext) {
|
|
|
+ if (this.isFullscreen) {
|
|
|
+ this.livePlayerContext.requestFullScreen({
|
|
|
+ direction: 90, // 横屏
|
|
|
+ success: () => {
|
|
|
+ console.log('进入全屏模式成功')
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.error('进入全屏模式失败:', err)
|
|
|
+ }
|
|
|
+ })
|
|
|
} else {
|
|
|
- this.videoContext.exitFullScreen();
|
|
|
+ this.livePlayerContext.exitFullScreen({
|
|
|
+ success: () => {
|
|
|
+ console.log('退出全屏模式成功')
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.error('退出全屏模式失败:', err)
|
|
|
+ }
|
|
|
+ })
|
|
|
}
|
|
|
- this.isFullscreen = !this.isFullscreen;
|
|
|
}
|
|
|
+ // #endif
|
|
|
+ },
|
|
|
+
|
|
|
+ // 截图
|
|
|
+ takeScreenshot() {
|
|
|
+ // #ifdef H5
|
|
|
+ if (this.$refs.jessibucaRef && this.isPlaying) {
|
|
|
+ this.$refs.jessibucaRef.screenshot()
|
|
|
+ uni.showToast({
|
|
|
+ title: '截图已保存',
|
|
|
+ icon: 'success'
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ uni.showToast({
|
|
|
+ title: '请先播放视频',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ if (this.livePlayerContext && this.isPlaying) {
|
|
|
+ this.livePlayerContext.snapshot({
|
|
|
+ success: (res) => {
|
|
|
+ console.log('截图成功:', res.tempImagePath)
|
|
|
+ // 可以保存到相册或做其他处理
|
|
|
+ uni.saveImageToPhotosAlbum({
|
|
|
+ filePath: res.tempImagePath,
|
|
|
+ success: () => {
|
|
|
+ uni.showToast({
|
|
|
+ title: '截图已保存到相册',
|
|
|
+ icon: 'success'
|
|
|
+ })
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.error('保存截图失败:', err)
|
|
|
+ uni.showToast({
|
|
|
+ title: '保存截图失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.error('截图失败:', err)
|
|
|
+ uni.showToast({
|
|
|
+ title: '截图失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ uni.showToast({
|
|
|
+ title: '请先播放视频',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+ },
|
|
|
+
|
|
|
+ // 小程序播放器状态变化处理
|
|
|
+ onStateChange(e) {
|
|
|
+ console.log('播放器状态变化:', e.detail)
|
|
|
+ const state = e.detail.code
|
|
|
+ // 根据状态码处理
|
|
|
+ switch (state) {
|
|
|
+ case 2001: // 已连接服务器
|
|
|
+ console.log('已连接服务器')
|
|
|
+ break
|
|
|
+ case 2002: // 已连接服务器,开始拉流
|
|
|
+ console.log('开始拉流')
|
|
|
+ break
|
|
|
+ case 2003: // 网络接收到首个视频帧
|
|
|
+ console.log('网络接收到首个视频帧')
|
|
|
+ break
|
|
|
+ case 2004: // 视频播放开始
|
|
|
+ console.log('视频播放开始')
|
|
|
+ break
|
|
|
+ case 2005: // 视频播放进度
|
|
|
+ console.log('视频播放进度')
|
|
|
+ break
|
|
|
+ case 2006: // 视频播放结束
|
|
|
+ console.log('视频播放结束')
|
|
|
+ this.isPlaying = false
|
|
|
+ break
|
|
|
+ case 2007: // 视频播放Loading
|
|
|
+ console.log('视频播放Loading')
|
|
|
+ break
|
|
|
+ case 2008: // 解码器启动
|
|
|
+ console.log('解码器启动')
|
|
|
+ break
|
|
|
+ case 2009: // 视频分辨率改变
|
|
|
+ console.log('视频分辨率改变')
|
|
|
+ break
|
|
|
+ case -2301: // 网络断连,且重新连接亦不能恢复,播放器已停止
|
|
|
+ console.error('网络断连,且重新连接亦不能恢复,播放器已停止')
|
|
|
+ this.isPlaying = false
|
|
|
+ uni.showToast({
|
|
|
+ title: '网络断连,请重试',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ break
|
|
|
+ case -2302: // 获取加速拉流地址失败
|
|
|
+ console.error('获取加速拉流地址失败')
|
|
|
+ break
|
|
|
+ case 2101: // 当前视频帧解码失败
|
|
|
+ console.error('当前视频帧解码失败')
|
|
|
+ break
|
|
|
+ case 2102: // 当前音频帧解码失败
|
|
|
+ console.error('当前音频帧解码失败')
|
|
|
+ break
|
|
|
+ case 2103: // 网络断连, 已启动自动重连
|
|
|
+ console.warn('网络断连, 已启动自动重连')
|
|
|
+ break
|
|
|
+ case 2104: // 网络断连, 重连中...
|
|
|
+ console.warn('网络断连, 重连中...')
|
|
|
+ break
|
|
|
+ case 2105: // 网络断连, 重连成功
|
|
|
+ console.log('网络断连, 重连成功')
|
|
|
+ break
|
|
|
+ case 2106: // 网络断连, 重连失败
|
|
|
+ console.error('网络断连, 重连失败')
|
|
|
+ break
|
|
|
+ case 2107: // 播放器连接超时
|
|
|
+ console.error('播放器连接超时')
|
|
|
+ break
|
|
|
+ case 2108: // 获取点播文件信息失败
|
|
|
+ console.error('获取点播文件信息失败')
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ console.log('其他状态:', state)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 小程序全屏状态变化
|
|
|
+ onFullscreenChange(e) {
|
|
|
+ this.isFullscreen = e.detail.fullScreen
|
|
|
+ console.log('全屏状态变化:', this.isFullscreen)
|
|
|
+ },
|
|
|
+
|
|
|
+ // 初始化流地址
|
|
|
+ initStreamUrl() {
|
|
|
+ const originalUrl = this.deviceInfo.originalStreamUrl
|
|
|
+ // 使用配置中的流服务器信息
|
|
|
+ const streamUrls = buildPlatformStreamUrls(originalUrl, {
|
|
|
+ streamServer: config.streamServer,
|
|
|
+ fallbackToHls: true
|
|
|
+ })
|
|
|
+
|
|
|
+ // #ifdef H5
|
|
|
+ this.deviceInfo.streamUrl = streamUrls.h5Url
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ // 检查是否可以在小程序中播放
|
|
|
+ if (streamUrls.miniProgramUrl) {
|
|
|
+ this.deviceInfo.streamUrl = streamUrls.miniProgramUrl
|
|
|
+ } else {
|
|
|
+ // 如果没有可用的小程序播放地址,显示提示
|
|
|
+ uni.showToast({
|
|
|
+ title: '当前视频流不支持小程序播放',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 3000
|
|
|
+ })
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ console.log('初始化流地址:', this.deviceInfo.streamUrl)
|
|
|
},
|
|
|
|
|
|
// 切换九宫格视图
|
|
|
@@ -359,22 +750,6 @@ export default {
|
|
|
})
|
|
|
},
|
|
|
|
|
|
- // 截图
|
|
|
- takeScreenshot() {
|
|
|
- // 模拟截图功能
|
|
|
- uni.showLoading({
|
|
|
- title: '截图中...'
|
|
|
- })
|
|
|
-
|
|
|
- setTimeout(() => {
|
|
|
- uni.hideLoading()
|
|
|
- uni.showToast({
|
|
|
- title: '截图已保存',
|
|
|
- icon: 'success'
|
|
|
- })
|
|
|
- }, 1000)
|
|
|
- },
|
|
|
-
|
|
|
// 语音对讲
|
|
|
toggleVoiceIntercom() {
|
|
|
this.isVoiceActive = !this.isVoiceActive
|
|
|
@@ -463,13 +838,32 @@ export default {
|
|
|
})
|
|
|
},
|
|
|
|
|
|
- // 处理视频错误
|
|
|
- handleVideoError(e) {
|
|
|
- console.error('视频播放错误:', e)
|
|
|
+ // 视频播放错误处理
|
|
|
+ onVideoError(e) {
|
|
|
+ console.error('视频播放错误:', e);
|
|
|
uni.showToast({
|
|
|
- title: '视频播放出错,请稍后再试',
|
|
|
- icon: 'none'
|
|
|
- })
|
|
|
+ title: '视频加载失败,请检查网络连接',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 2000
|
|
|
+ });
|
|
|
+
|
|
|
+ // 在H5环境,尝试重新加载或使用备用流
|
|
|
+ // #ifdef H5
|
|
|
+ setTimeout(() => {
|
|
|
+ if (this.isPlaying && this.$refs.jessibucaRef) {
|
|
|
+ console.log('尝试重新加载视频流');
|
|
|
+ // 可以尝试使用备用流地址
|
|
|
+ if (this.deviceInfo.originalStreamUrl !== config.streamServer.wsFlvServer) {
|
|
|
+ this.deviceInfo.streamUrl = config.streamServer.wsFlvServer;
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (this.$refs.jessibucaRef) {
|
|
|
+ this.$refs.jessibucaRef.play();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, 3000);
|
|
|
+ // #endif
|
|
|
},
|
|
|
|
|
|
// 更新时间
|
|
|
@@ -482,7 +876,62 @@ export default {
|
|
|
const seconds = String(now.getSeconds()).padStart(2, '0')
|
|
|
this.currentTime = `${hours}:${minutes}:${seconds}`
|
|
|
}, 1000)
|
|
|
- }
|
|
|
+ },
|
|
|
+ // 设置全屏状态监听
|
|
|
+ setupFullscreenListener() {
|
|
|
+ this.fullscreenChangeHandler = () => {
|
|
|
+ const isFullscreen = !!(
|
|
|
+ document.fullscreenElement ||
|
|
|
+ document.mozFullScreenElement ||
|
|
|
+ document.webkitFullscreenElement ||
|
|
|
+ document.msFullscreenElement
|
|
|
+ );
|
|
|
+
|
|
|
+ if (this.isFullscreen !== isFullscreen) {
|
|
|
+ this.isFullscreen = isFullscreen;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // document.addEventListener('fullscreenchange', this.fullscreenChangeHandler);
|
|
|
+ // document.addEventListener('webkitfullscreenchange', this.fullscreenChangeHandler);
|
|
|
+ // document.addEventListener('mozfullscreenchange', this.fullscreenChangeHandler);
|
|
|
+ // document.addEventListener('MSFullscreenChange', this.fullscreenChangeHandler);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 移除全屏状态监听
|
|
|
+ removeFullscreenListener() {
|
|
|
+ if (this.fullscreenChangeHandler) {
|
|
|
+ // document.removeEventListener('fullscreenchange', this.fullscreenChangeHandler);
|
|
|
+ // document.removeEventListener('webkitfullscreenchange', this.fullscreenChangeHandler);
|
|
|
+ // document.removeEventListener('mozfullscreenchange', this.fullscreenChangeHandler);
|
|
|
+ // document.removeEventListener('MSFullscreenChange', this.fullscreenChangeHandler);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 设置键盘监听
|
|
|
+ setupKeyboardListener() {
|
|
|
+ this.keydownHandler = (event) => {
|
|
|
+ // ESC键退出全屏
|
|
|
+ if (event.key === 'Escape' && this.isFullscreen) {
|
|
|
+ this.setFullscreen(false);
|
|
|
+ }
|
|
|
+
|
|
|
+ // F键切换全屏
|
|
|
+ if (event.key === 'f' || event.key === 'F') {
|
|
|
+ this.toggleFullscreen();
|
|
|
+ event.preventDefault();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // document.addEventListener('keydown', this.keydownHandler);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 移除键盘监听
|
|
|
+ removeKeyboardListener() {
|
|
|
+ if (this.keydownHandler) {
|
|
|
+ // document.removeEventListener('keydown', this.keydownHandler);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
@@ -614,6 +1063,8 @@ export default {
|
|
|
/* 视频预览区域 */
|
|
|
.video-section {
|
|
|
margin: 0 30rpx 20rpx;
|
|
|
+ position: relative;
|
|
|
+ z-index: 1;
|
|
|
}
|
|
|
|
|
|
.video-container {
|
|
|
@@ -624,11 +1075,39 @@ export default {
|
|
|
border-radius: 16rpx;
|
|
|
overflow: hidden;
|
|
|
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.1);
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.video-container.fullscreen-mode {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100vw;
|
|
|
+ height: 100vh;
|
|
|
+ margin: 0;
|
|
|
+ z-index: 9999;
|
|
|
+ border-radius: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.video-container.fullscreen-mode .video-controls {
|
|
|
+ padding: 30rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.video-container.fullscreen-mode .top-controls,
|
|
|
+.video-container.fullscreen-mode .bottom-controls {
|
|
|
+ opacity: 0;
|
|
|
+ transition: opacity 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.video-container.fullscreen-mode:hover .top-controls,
|
|
|
+.video-container.fullscreen-mode:hover .bottom-controls {
|
|
|
+ opacity: 1;
|
|
|
}
|
|
|
|
|
|
.video-player, .video-placeholder {
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
+ object-fit: contain;
|
|
|
}
|
|
|
|
|
|
.video-controls {
|