|
|
@@ -164,8 +164,8 @@
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
- <view class="liveMediaWrap">
|
|
|
- <image
|
|
|
+
|
|
|
+ <!-- <image
|
|
|
v-if="traceDetail.camera.coverImage"
|
|
|
class="liveCover"
|
|
|
:src="traceDetail.camera.coverImage"
|
|
|
@@ -179,13 +179,93 @@
|
|
|
</view>
|
|
|
<text class="livePlaceholderMain">实时画面接入中</text>
|
|
|
<text class="livePlaceholderSub">请稍候查看现场情况</text>
|
|
|
- </view>
|
|
|
-
|
|
|
+ </view> -->
|
|
|
+ <!-- 视频预览区域 -->
|
|
|
+ <view class="video-section">
|
|
|
+ <view class="video-container" :class="{'fullscreen-mode': isFullscreen}">
|
|
|
+ <image v-if="!isPlaying" src="/static/images/video-placeholder.jpg" mode="aspectFill"
|
|
|
+ class="video-placeholder"></image>
|
|
|
+
|
|
|
+ <!-- 使用跨平台视频播放组件 -->
|
|
|
+ <!-- #ifdef H5 -->
|
|
|
+ <view v-if="isPlaying" class="h5-video-wrapper">
|
|
|
+ <Jessibuca ref="jessibucaRef" :videoUrl="getH5StreamUrl" :hasAudio="true" @error="onVideoError" />
|
|
|
+ </view>
|
|
|
+ <!-- #endif -->
|
|
|
+
|
|
|
+ <!-- 微信小程序视频播放 -->
|
|
|
+ <!-- #ifdef MP-WEIXIN -->
|
|
|
+ <video
|
|
|
+ v-if="isPlaying"
|
|
|
+ id="myVideo"
|
|
|
+ class="video-player"
|
|
|
+ :src="getAppStreamUrl"
|
|
|
+ :autoplay="true"
|
|
|
+ :controls="true"
|
|
|
+ :show-center-play-btn="true"
|
|
|
+ :enable-progress-gesture="false"
|
|
|
+ :object-fit="'contain'"
|
|
|
+ @error="onVideoError"
|
|
|
+ @play="onVideoPlay"
|
|
|
+ @pause="onVideoPause"
|
|
|
+ @ended="onVideoEnded"
|
|
|
+ @timeupdate="onTimeUpdate"
|
|
|
+ @fullscreenchange="onFullscreenChange"
|
|
|
+ ></video>
|
|
|
+ <!-- #endif -->
|
|
|
+ <!-- App端视频播放 -->
|
|
|
+ <!-- #ifdef APP-PLUS || APP-HARMONY -->
|
|
|
+ <video
|
|
|
+ v-if="isPlaying"
|
|
|
+ id="appVideo"
|
|
|
+ class="video-player"
|
|
|
+ :src="getAppStreamUrl"
|
|
|
+ :autoplay="true"
|
|
|
+ :controls="true"
|
|
|
+ :show-center-play-btn="true"
|
|
|
+ @error="onVideoError"
|
|
|
+ ></video>
|
|
|
+ <!-- #endif -->
|
|
|
+
|
|
|
+ <!-- 视频控制层 -->
|
|
|
+ <view class="video-controls">
|
|
|
+ <view class="control-row top-controls">
|
|
|
+ <view class="signal-indicator">
|
|
|
+ <image src="/static/icons/signal_icon.png" mode="aspectFit"
|
|
|
+ style="width: 16px; height: 16px;"></image>
|
|
|
+ <text class="signal-text">信号良好</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="fullscreen-button" @click="toggleFullscreen">
|
|
|
+ <image src="/static/icons/resize_icon.png" mode="aspectFit"
|
|
|
+ style="width: 20px; height: 20px;"></image>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="control-row center-controls">
|
|
|
+ <view v-if="!isPlaying" class="play-button" @click="togglePlayState">
|
|
|
+ <image src="/static/icons/play_icon.png" mode="aspectFit"
|
|
|
+ style="width: 32px; height: 32px;"></image>
|
|
|
+ </view>
|
|
|
+ <view v-else class="center-button-container" @click="togglePlayState">
|
|
|
+ <view class="pause-icon">
|
|
|
+ <image src="/static/icons/pause_icon.png" mode="aspectFit"
|
|
|
+ style="width: 24px; height: 24px;"></image>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="control-row bottom-controls">
|
|
|
+ <view class="video-time">{{ currentTime }}</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
<view class="liveStatusPill" :class="cameraStatus.value">
|
|
|
<view class="liveStatusDot" />
|
|
|
<text>{{ cameraStatusText }}</text>
|
|
|
</view>
|
|
|
- </view>
|
|
|
+
|
|
|
|
|
|
<view class="liveDesc">
|
|
|
<text>{{ traceDetail.camera.desc }}</text>
|
|
|
@@ -362,9 +442,16 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { computed, ref } from 'vue'
|
|
|
+import { computed, ref, reactive } from 'vue'
|
|
|
import { onLoad } from '@dcloudio/uni-app'
|
|
|
import {getTraceDetail } from '@/api/base/index.js'
|
|
|
+import {
|
|
|
+ getDeviceCollectorDetail,
|
|
|
+ getChannels,
|
|
|
+ playStart,
|
|
|
+ pause ,
|
|
|
+} from '@/api/device.js'
|
|
|
+import Jessibuca from '@/components/common/jessibuca.vue'
|
|
|
// 页面状态选择(H5 演示用):通过路由参数传入 state 即可切换 mock 场景
|
|
|
// 例如:/pages/trace/detail?state=reportPending
|
|
|
// 加载状态
|
|
|
@@ -373,7 +460,31 @@ const loading = ref(false)
|
|
|
const traceInfo = ref(null)
|
|
|
const routeOptions = ref({})
|
|
|
const mockStateKey = ref('normal')
|
|
|
-
|
|
|
+const isFullscreen = ref(false)
|
|
|
+const isPlaying = ref(false)
|
|
|
+const livePlayerContext = ref(null) // 小程序视频上下文
|
|
|
+const appLivePlayerContext = ref(null) // App端视频上下文
|
|
|
+const jessibucaRef = ref(null)
|
|
|
+
|
|
|
+const isMuted = ref(false)
|
|
|
+const isRecording = ref(false)
|
|
|
+const isVoiceActive = ref(false)
|
|
|
+const isGridView = ref(false)
|
|
|
+const isZoomMode = ref(false)
|
|
|
+const currentTime = ref('14:30:25')
|
|
|
+// 响应式数据
|
|
|
+const deviceInfo = reactive({
|
|
|
+ deviceId: '34020000001110000001',
|
|
|
+ name: '设备加载中...',
|
|
|
+ status: 'off',
|
|
|
+ location: '正在获取位置...',
|
|
|
+ lastUpdate: '',
|
|
|
+ deviceType: 'weather', // 默认类型,会根据API返回更新
|
|
|
+ deviceTypeId: null,
|
|
|
+ streamUrl: '',
|
|
|
+ channelId: null, // 当前通道ID
|
|
|
+ originalStreamUrl: '',
|
|
|
+})
|
|
|
const MOCK_TRACE_DETAILS = {
|
|
|
normal: {
|
|
|
product: {
|
|
|
@@ -619,8 +730,30 @@ const MOCK_TRACE_DETAILS = {
|
|
|
farmTimeline: []
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
-
|
|
|
+// 加载Jessibuca脚本
|
|
|
+ const loadJessibucaScript = () => {
|
|
|
+ // #ifdef H5
|
|
|
+ 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)
|
|
|
+ // #endif
|
|
|
+ }
|
|
|
+// 获取H5环境使用的流地址
|
|
|
+ const getH5StreamUrl = computed(() => {
|
|
|
+ // 确保使用安全的 WSS 协议
|
|
|
+ let url = deviceInfo.originalStreamUrl
|
|
|
+ if (url && url.startsWith('ws://')) {
|
|
|
+ console.warn('检测到不安全的 ws:// 协议,自动转换为 wss://')
|
|
|
+ url = url.replace('ws://', 'wss://')
|
|
|
+ }
|
|
|
+ return url
|
|
|
+ })
|
|
|
function resolveStateKey(opts) {
|
|
|
const raw = (opts?.state || opts?.batchState || opts?.scene || '').toString().trim()
|
|
|
if (!raw) return 'normal'
|
|
|
@@ -634,13 +767,91 @@ onLoad((opts) => {
|
|
|
const batchId = fullPath.split('/').filter(Boolean).pop()
|
|
|
|
|
|
// 优先使用路由参数中的 id,其次使用 URL 路径中的 id
|
|
|
- const finalId = batchId || 1
|
|
|
+ const finalId = batchId || 4
|
|
|
loadData(finalId)
|
|
|
-
|
|
|
+ queryChannels()
|
|
|
routeOptions.value = opts || {}
|
|
|
mockStateKey.value = resolveStateKey(opts || {})
|
|
|
+ startTimeUpdate()
|
|
|
+
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ setTimeout(() => {
|
|
|
+ livePlayerContext.value = uni.createVideoContext('myVideo')
|
|
|
+ console.log('微信小程序视频上下文已创建')
|
|
|
+ }, 500)
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef APP-PLUS || APP-HARMONY
|
|
|
+ setTimeout(() => {
|
|
|
+ appLivePlayerContext.value = uni.createVideoContext('appVideo')
|
|
|
+ console.log('App视频上下文已创建')
|
|
|
+ }, 500)
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef H5
|
|
|
+ loadJessibucaScript()
|
|
|
+ // #endif
|
|
|
})
|
|
|
-
|
|
|
+// 根据设备id获取通道列表
|
|
|
+ const queryChannels = () => {
|
|
|
+ getChannels(deviceInfo.deviceId)
|
|
|
+ .then(response => {
|
|
|
+ console.log('获取通道列表:', response)
|
|
|
+ const res = response.data
|
|
|
+ if (res.code === 0 && res.data.total > 0) {
|
|
|
+ const channels = res.data.list
|
|
|
+ deviceInfo.channelId = channels[0].deviceId
|
|
|
+ deviceInfo.status = channels[0].status
|
|
|
+ playStart(deviceInfo.deviceId, deviceInfo.channelId).then(res => {
|
|
|
+ if (res.data.code !== 0) {
|
|
|
+ console.error('播放开始失败:', res.message)
|
|
|
+ uni.showToast({
|
|
|
+ title: '播放失败: ' + res.message,
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ console.log('播放开始:', res)
|
|
|
+
|
|
|
+ // #ifdef H5
|
|
|
+ let streamUrl = res.data.data.wss_flv
|
|
|
+
|
|
|
+ if (streamUrl) {
|
|
|
+ const urlObj = new URL(streamUrl)
|
|
|
+ // 替换 hostname
|
|
|
+ urlObj.hostname = 'nxy.gbdfarm.com'
|
|
|
+ // 替换端口
|
|
|
+ urlObj.port = '9000'
|
|
|
+
|
|
|
+ deviceInfo.originalStreamUrl = urlObj.toString()
|
|
|
+ console.log("queryChannels - deviceInfo.originalStreamUrl", deviceInfo.originalStreamUrl)
|
|
|
+ } else {
|
|
|
+ console.warn('未获取到 wss_flv 流地址')
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ deviceInfo.originalStreamUrl = res.data.data.hls || deviceInfo.originalStreamUrl
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef APP-PLUS || APP-HARMONY
|
|
|
+ deviceInfo.originalStreamUrl = res.data.data.fmp4 || deviceInfo.originalStreamUrl
|
|
|
+ // #endif
|
|
|
+ }).catch(err => {
|
|
|
+ console.error('播放开始失败:', err)
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ uni.showToast({
|
|
|
+ title: '获取通道列表失败: ' + res.data.message,
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(err => {
|
|
|
+ console.error('获取通道列表错误:', err)
|
|
|
+ })
|
|
|
+ }
|
|
|
const loadData = async (batchId) => {
|
|
|
loading.value = true
|
|
|
|
|
|
@@ -657,7 +868,456 @@ const loadData = async (batchId) => {
|
|
|
loading.value = false
|
|
|
}
|
|
|
}
|
|
|
+// 视频播放错误处理
|
|
|
+ const onVideoError = (e) => {
|
|
|
+ console.error('视频播放错误:', e)
|
|
|
+
|
|
|
+ let errorMsg = '视频加载失败'
|
|
|
+
|
|
|
+ // 微信小程序video组件错误码
|
|
|
+ if (e && e.detail) {
|
|
|
+ const errCode = e.detail.errCode
|
|
|
+ switch(errCode) {
|
|
|
+ case 10001:
|
|
|
+ errorMsg = '网络错误,请检查网络连接'
|
|
|
+ break
|
|
|
+ case 10002:
|
|
|
+ errorMsg = '视频格式不支持'
|
|
|
+ break
|
|
|
+ case 10003:
|
|
|
+ errorMsg = '视频解码失败'
|
|
|
+ break
|
|
|
+ case 10004:
|
|
|
+ errorMsg = '视频地址无效'
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ errorMsg = `播放失败(错误码:${errCode})`
|
|
|
+ }
|
|
|
+ console.error('视频错误详情:', e.detail)
|
|
|
+ }
|
|
|
+
|
|
|
+ uni.showToast({
|
|
|
+ title: errorMsg,
|
|
|
+ icon: 'none',
|
|
|
+ duration: 3000
|
|
|
+ })
|
|
|
+
|
|
|
+ isPlaying.value = false
|
|
|
+
|
|
|
+ // #ifdef H5
|
|
|
+ setTimeout(() => {
|
|
|
+ if (isPlaying.value && jessibucaRef.value) {
|
|
|
+ console.log('尝试重新加载视频流')
|
|
|
+ if (deviceInfo.originalStreamUrl !== config.streamServer.wsFlvServer) {
|
|
|
+ deviceInfo.streamUrl = config.streamServer.wsFlvServer
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, 3000)
|
|
|
+ // #endif
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新时间
|
|
|
+ const startTimeUpdate = () => {
|
|
|
+ setInterval(() => {
|
|
|
+ const now = new Date()
|
|
|
+ const hours = String(now.getHours()).padStart(2, '0')
|
|
|
+ const minutes = String(now.getMinutes()).padStart(2, '0')
|
|
|
+ const seconds = String(now.getSeconds()).padStart(2, '0')
|
|
|
+ currentTime.value = `${hours}:${minutes}:${seconds}`
|
|
|
+ }, 1000)
|
|
|
+ }
|
|
|
+// 播放/暂停切换
|
|
|
+ const togglePlayState = () => {
|
|
|
+ if (!isPlaying.value) {
|
|
|
+ isPlaying.value = true
|
|
|
+
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ setTimeout(() => {
|
|
|
+ if (livePlayerContext.value) {
|
|
|
+ livePlayerContext.value.play()
|
|
|
+ console.log('微信小程序开始播放')
|
|
|
+ }
|
|
|
+ }, 300)
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef APP-PLUS || APP-HARMONY
|
|
|
+ setTimeout(() => {
|
|
|
+ if (appLivePlayerContext.value) {
|
|
|
+ appLivePlayerContext.value.play()
|
|
|
+ console.log('App开始播放')
|
|
|
+ }
|
|
|
+ }, 300)
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef H5
|
|
|
+ setTimeout(() => {
|
|
|
+ playStart(deviceInfo.deviceId, deviceInfo.channelId).then(res => {
|
|
|
+ if (res.data.code !== 0) {
|
|
|
+ console.error('播放开始失败:', res.message)
|
|
|
+ uni.showToast({
|
|
|
+ title: '播放失败: ' + res.message,
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ console.log('播放开始:', res)
|
|
|
+
|
|
|
+ // 使用 wss_flv 并替换域名和端口
|
|
|
+ let streamUrl = res.data.data.wss_flv
|
|
|
+ if (streamUrl) {
|
|
|
+ const urlObj = new URL(streamUrl)
|
|
|
+ urlObj.hostname = 'nxy.gbdfarm.com'
|
|
|
+ urlObj.port = '9000'
|
|
|
+ deviceInfo.originalStreamUrl = urlObj.toString()
|
|
|
+ console.log("togglePlayState - deviceInfo.originalStreamUrl", deviceInfo.originalStreamUrl)
|
|
|
+ }
|
|
|
+ }).catch(err => {
|
|
|
+ console.error('播放开始失败:', err)
|
|
|
+ })
|
|
|
+ uni.vibrateShort()
|
|
|
+ }, 300)
|
|
|
+ // #endif
|
|
|
+ } else {
|
|
|
+ // #ifdef H5
|
|
|
+ if (jessibucaRef.value) {
|
|
|
+ jessibucaRef.value.pause()
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ if (livePlayerContext.value) {
|
|
|
+ livePlayerContext.value.pause()
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef APP-PLUS || APP-HARMONY
|
|
|
+ if (appLivePlayerContext.value) {
|
|
|
+ appLivePlayerContext.value.pause()
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ isPlaying.value = false
|
|
|
+
|
|
|
+ uni.showToast({
|
|
|
+ title: '视频已暂停',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 1500
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 静音切换
|
|
|
+ const toggleMute = () => {
|
|
|
+ isMuted.value = !isMuted.value
|
|
|
+
|
|
|
+ // #ifdef H5
|
|
|
+ if (jessibucaRef.value) {
|
|
|
+ if (isMuted.value) {
|
|
|
+ jessibucaRef.value.mute()
|
|
|
+ } else {
|
|
|
+ jessibucaRef.value.cancelMute()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifndef H5
|
|
|
+ // App端的静音通过 live-player 的 muted 属性控制,会自动响应
|
|
|
+ uni.showToast({
|
|
|
+ title: isMuted.value ? '已静音' : '已取消静音',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ // #endif
|
|
|
+ }
|
|
|
+
|
|
|
+ // 全屏切换
|
|
|
+ const toggleFullscreen = () => {
|
|
|
+ if (!isPlaying.value) {
|
|
|
+ togglePlayState()
|
|
|
+ setTimeout(() => {
|
|
|
+ setFullscreen(true)
|
|
|
+ }, 500)
|
|
|
+ } else {
|
|
|
+ setFullscreen(!isFullscreen.value)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置全屏状态
|
|
|
+ const setFullscreen = (fullscreen) => {
|
|
|
+ isFullscreen.value = fullscreen
|
|
|
+
|
|
|
+ // #ifdef H5
|
|
|
+ if (isFullscreen.value) {
|
|
|
+ const ua = navigator.userAgent.toLowerCase()
|
|
|
+ const isMobile = /mobile|android|iphone|ipad/.test(ua)
|
|
|
+
|
|
|
+ if (isMobile && jessibucaRef.value) {
|
|
|
+ jessibucaRef.value.fullscreenSwich()
|
|
|
+ } else {
|
|
|
+ setTimeout(() => {
|
|
|
+ if (jessibucaRef.value) {
|
|
|
+ jessibucaRef.value.resize()
|
|
|
+ }
|
|
|
+ }, 300)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (window.screen && window.screen.orientation && window.screen.orientation.lock) {
|
|
|
+ window.screen.orientation.lock('landscape').catch(err => {
|
|
|
+ console.error('无法锁定屏幕方向:', err)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (jessibucaRef.value) {
|
|
|
+ if (jessibucaRef.value.isFullscreen()) {
|
|
|
+ jessibucaRef.value.fullscreenSwich()
|
|
|
+ }
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ if (jessibucaRef.value) {
|
|
|
+ jessibucaRef.value.resize()
|
|
|
+ }
|
|
|
+ }, 300)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (window.screen && window.screen.orientation && window.screen.orientation.unlock) {
|
|
|
+ window.screen.orientation.unlock()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ if (livePlayerContext.value) {
|
|
|
+ if (isFullscreen.value) {
|
|
|
+ livePlayerContext.value.requestFullScreen({
|
|
|
+ direction: 90
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ livePlayerContext.value.exitFullScreen()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef APP-PLUS || APP-HARMONY
|
|
|
+ if (appLivePlayerContext.value) {
|
|
|
+ if (isFullscreen.value) {
|
|
|
+ appLivePlayerContext.value.requestFullScreen({
|
|
|
+ direction: 90
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ appLivePlayerContext.value.exitFullScreen()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+ }
|
|
|
+
|
|
|
+ // 截图
|
|
|
+ const takeScreenshot = () => {
|
|
|
+ // #ifdef H5
|
|
|
+ if (jessibucaRef.value && isPlaying.value) {
|
|
|
+ jessibucaRef.value.screenshot()
|
|
|
+ uni.showToast({
|
|
|
+ title: '截图已保存',
|
|
|
+ icon: 'success'
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ uni.showToast({
|
|
|
+ title: '请先播放视频',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+
|
|
|
+
|
|
|
+ // if (livePlayerContext.value && isPlaying.value) {
|
|
|
+ // livePlayerContext.value.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'
|
|
|
+ // })
|
|
|
+ // }
|
|
|
+
|
|
|
+
|
|
|
+ // #ifndef H5
|
|
|
+ if (appLivePlayerContext.value && isPlaying.value) {
|
|
|
+ appLivePlayerContext.value.snapshot({
|
|
|
+ success: (res) => {
|
|
|
+ console.log('App截图成功:', res.tempImagePath)
|
|
|
+ uni.saveImageToPhotosAlbum({
|
|
|
+ filePath: res.tempImagePath,
|
|
|
+ success: () => {
|
|
|
+ uni.showToast({
|
|
|
+ title: '截图已保存到相册',
|
|
|
+ icon: 'success'
|
|
|
+ })
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.error('App保存截图失败:', err)
|
|
|
+ uni.showToast({
|
|
|
+ title: '保存截图失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.error('App截图失败:', err)
|
|
|
+ uni.showToast({
|
|
|
+ title: '截图失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ uni.showToast({
|
|
|
+ title: '请先播放视频',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+ }
|
|
|
+
|
|
|
+ // 小程序播放器状态变化处理
|
|
|
+ const onVideoPlay = () => {
|
|
|
+ console.log('视频开始播放')
|
|
|
+ isPlaying.value = true
|
|
|
+ }
|
|
|
+
|
|
|
+ const onVideoPause = () => {
|
|
|
+ console.log('视频暂停')
|
|
|
+ }
|
|
|
+
|
|
|
+ const onVideoEnded = () => {
|
|
|
+ console.log('视频播放结束')
|
|
|
+ isPlaying.value = false
|
|
|
+ }
|
|
|
+
|
|
|
+ const onTimeUpdate = (e) => {
|
|
|
+ // 更新播放时间
|
|
|
+ if (e && e.detail) {
|
|
|
+ const currentSeconds = Math.floor(e.detail.currentTime)
|
|
|
+ const hours = String(Math.floor(currentSeconds / 3600)).padStart(2, '0')
|
|
|
+ const minutes = String(Math.floor((currentSeconds % 3600) / 60)).padStart(2, '0')
|
|
|
+ const seconds = String(currentSeconds % 60).padStart(2, '0')
|
|
|
+ currentTime.value = `${hours}:${minutes}:${seconds}`
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const 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('视频播放结束')
|
|
|
+ isPlaying.value = false
|
|
|
+ break
|
|
|
+ case 2007:
|
|
|
+ console.log('视频播放Loading')
|
|
|
+ break
|
|
|
+ case 2008:
|
|
|
+ console.log('解码器启动')
|
|
|
+ break
|
|
|
+ case 2009:
|
|
|
+ console.log('视频分辨率改变')
|
|
|
+ break
|
|
|
+ case -2301:
|
|
|
+ console.error('网络断连,且重新连接亦不能恢复,播放器已停止')
|
|
|
+ isPlaying.value = 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)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 小程序全屏状态变化
|
|
|
+ const onFullscreenChange = (e) => {
|
|
|
+ isFullscreen.value = e.detail.fullScreen
|
|
|
+ console.log('全屏状态变化:', isFullscreen.value)
|
|
|
+ }
|
|
|
+// 获取App(安卓/鸿蒙)环境使用的流地址
|
|
|
+ const getAppStreamUrl = computed(() => {
|
|
|
+ // 优先使用 fmp4 格式(微信小程序和App都支持)
|
|
|
+ if (deviceInfo.originalStreamUrl) {
|
|
|
+ console.log("当前视频流地址:", deviceInfo.originalStreamUrl)
|
|
|
+ return deviceInfo.originalStreamUrl
|
|
|
+ }
|
|
|
|
|
|
+ // 如果没有流地址,返回空字符串
|
|
|
+ console.warn('未获取到视频流地址')
|
|
|
+ return ''
|
|
|
+ })
|
|
|
const traceDetail = computed(() => {
|
|
|
// 如果没有真实数据,返回 mock 数据
|
|
|
if (!traceInfo.value) {
|
|
|
@@ -866,18 +1526,18 @@ const traceDetail = computed(() => {
|
|
|
|
|
|
// 种植现场状态:三态逻辑
|
|
|
const cameraStatus = computed(() => {
|
|
|
- const camera = traceDetail.value?.camera
|
|
|
- if (!camera) return 'offline'
|
|
|
+ const camera = deviceInfo?.status
|
|
|
+ if (!camera) return 'off'
|
|
|
|
|
|
- const hasLiveUrl = !!camera.liveUrl
|
|
|
+ // const hasLiveUrl = !!camera.liveUrl
|
|
|
|
|
|
// 有 liveUrl 但无封面图 → loading(信号接入中)
|
|
|
// 有 liveUrl 且有封面图 → online(可播放)
|
|
|
// 无 liveUrl → offline
|
|
|
- if (hasLiveUrl && !camera.coverImage) {
|
|
|
+ if (camera && camera == 'off') {
|
|
|
return 'loading'
|
|
|
}
|
|
|
- if (hasLiveUrl || camera.coverImage) {
|
|
|
+ if (camera && camera == 'ON') {
|
|
|
return 'online'
|
|
|
}
|
|
|
return 'offline'
|
|
|
@@ -2283,4 +2943,206 @@ function previewDoc(kind, index) {
|
|
|
color: rgba(88, 100, 92, 0.3);
|
|
|
margin-top: 8rpx;
|
|
|
}
|
|
|
+/* 视频预览区域 */
|
|
|
+ .video-section {
|
|
|
+ /* margin: 0 30rpx 20rpx; */
|
|
|
+ position: relative;
|
|
|
+ z-index: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .video-container {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: 420rpx;
|
|
|
+ background-color: #000000;
|
|
|
+ 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组件样式 */
|
|
|
+ #myVideo {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background-color: #000000;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /* App端video组件样式 */
|
|
|
+ /* #ifdef APP-PLUS || APP-HARMONY */
|
|
|
+ #appVideo {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background-color: #000000;
|
|
|
+ }
|
|
|
+ /* #endif */
|
|
|
+
|
|
|
+ .h5-video-wrapper {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* App端视频播放器样式优化 */
|
|
|
+ /* #ifdef APP-PLUS || APP-HARMONY */
|
|
|
+ #appVideoPlayer {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background-color: #000000;
|
|
|
+ }
|
|
|
+ /* #endif */
|
|
|
+
|
|
|
+ .video-controls {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 20rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+ background: linear-gradient(to bottom, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0) 30%, rgba(0, 0, 0, 0) 70%, rgba(0, 0, 0, 0.4) 100%);
|
|
|
+ }
|
|
|
+ .control-row {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .top-controls {
|
|
|
+ height: 80rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .center-controls {
|
|
|
+ height: 120rpx;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .bottom-controls {
|
|
|
+ height: 80rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .signal-indicator {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ color: #FFFFFF;
|
|
|
+ font-size: 24rpx;
|
|
|
+ background-color: rgba(0, 0, 0, 0.5);
|
|
|
+ padding: 8rpx 16rpx;
|
|
|
+ border-radius: 30rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .signal-indicator svg {
|
|
|
+ margin-right: 8rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .signal-text {
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .fullscreen-button {
|
|
|
+ width: 60rpx;
|
|
|
+ height: 60rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: #FFFFFF;
|
|
|
+ background-color: rgba(0, 0, 0, 0.5);
|
|
|
+ border-radius: 50%;
|
|
|
+ transition: all 0.2s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .fullscreen-button:active {
|
|
|
+ background-color: rgba(76, 175, 80, 0.7);
|
|
|
+ transform: scale(0.9);
|
|
|
+ }
|
|
|
+
|
|
|
+ .video-time {
|
|
|
+ color: #FFFFFF;
|
|
|
+ font-size: 26rpx;
|
|
|
+ background-color: rgba(0, 0, 0, 0.5);
|
|
|
+ padding: 6rpx 16rpx;
|
|
|
+ border-radius: 30rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .play-button {
|
|
|
+ width: 100rpx;
|
|
|
+ height: 100rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ background-color: rgba(255, 255, 255, 0.9);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.5);
|
|
|
+ transition: all 0.2s;
|
|
|
+ transform: scale(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .play-button:active {
|
|
|
+ transform: scale(0.92);
|
|
|
+ background-color: rgba(255, 255, 255, 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .center-button-container {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pause-icon {
|
|
|
+ width: 80rpx;
|
|
|
+ height: 80rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ background-color: rgba(0, 0, 0, 0.5);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ opacity: 0;
|
|
|
+ transition: opacity 0.3s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .center-button-container:active .pause-icon {
|
|
|
+ opacity: 1;
|
|
|
+ background-color: rgba(76, 175, 80, 0.7);
|
|
|
+ }
|
|
|
</style>
|