Преглед на файлове

1.监控设备增加摄像头视频播放;2.修复H5页面显示问题

jiuling преди 11 месеца
родител
ревизия
6f28e5b0b1

+ 22 - 6
App.vue

@@ -4,6 +4,13 @@ export default {
   onLaunch: function() {
     console.log('App Launch');
     this.checkLoginStatus();
+    // 添加全局页面生命周期监听
+    // uni.onPageNotFound(function(e) {
+    //   console.error('页面不存在:', e);
+    //   uni.navigateTo({
+    //     url: '/pages/dashboard/index'
+    //   });
+    // });
   },
   onShow: function() {
     console.log('App Show');
@@ -14,7 +21,7 @@ export default {
   methods: {
     checkLoginStatus() {
       // 登录状态检查,对敏感页面进行拦截
-      const pages = ['pages/dashboard/index', 'pages/user/index', 'pages/activity/index', 'pages/device/index']; 
+      const pages = ['pages/device/index']; 
       
       uni.addInterceptor('navigateTo', {
         invoke(e) {
@@ -52,11 +59,7 @@ export default {
 }
 </script>
 
-<template>
-  <view class="content">
-    <slot />
-  </view>
-</template>
+<!-- App.vue中不需要template,uni-app会自动处理页面跳转 -->
 
 <style lang="scss">
 /* 全局基础样式 */
@@ -80,4 +83,17 @@ button::after {
   height: 0;
   color: transparent;
 }
+
+/* H5环境特殊样式 */
+/* #ifdef H5 */
+html, body {
+  height: 100%;
+  width: 100%;
+  overflow-x: hidden;
+}
+
+.uni-page-head {
+  display: flex !important;
+}
+/* #endif */
 </style>

+ 434 - 0
components/common/jessibuca.vue

@@ -0,0 +1,434 @@
+<template>
+  <div
+    ref="container"
+    class="jessibuca-container"
+    :class="{'jessibuca-fullscreen': fullscreen}"
+    @dblclick="fullscreenSwich"
+  >
+    <div class="jessibuca-player-wrapper"></div>
+    <div id="buttonsBox" class="buttons-box">
+      <div class="buttons-box-left">
+        <i v-if="!playing" class="iconfont icon-play jessibuca-btn" @click="playBtnClick" />
+        <i v-if="playing" class="iconfont icon-pause jessibuca-btn" @click="pause" />
+        <i class="iconfont icon-stop jessibuca-btn" @click="destroy" />
+        <i v-if="isNotMute" class="iconfont icon-audio-high jessibuca-btn" @click="mute()" />
+        <i v-if="!isNotMute" class="iconfont icon-audio-mute jessibuca-btn" @click="cancelMute()" />
+      </div>
+      <div class="buttons-box-right">
+        <span class="jessibuca-btn">{{ kBps }} kb/s</span>
+        <!--          <i class="iconfont icon-file-record1 jessibuca-btn"></i>-->
+        <!--          <i class="iconfont icon-xiangqing2 jessibuca-btn" ></i>-->
+        <i
+          class="iconfont icon-camera1196054easyiconnet jessibuca-btn"
+          style="font-size: 1rem !important"
+          @click="screenshot"
+        />
+        <i class="iconfont icon-shuaxin11 jessibuca-btn" @click="playBtnClick" />
+        <i v-if="!fullscreen" class="iconfont icon-weibiaoti10 jessibuca-btn" @click="fullscreenSwich" />
+        <i v-if="fullscreen" class="iconfont icon-weibiaoti11 jessibuca-btn" @click="fullscreenSwich" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+const jessibucaPlayer = {}
+export default {
+  name: 'Jessibuca',
+  props: ['videoUrl', 'error', 'hasAudio', 'height'],
+  data() {
+    return {
+      playing: false,
+      isNotMute: false,
+      quieting: false,
+      fullscreen: false,
+      loaded: false, // mute
+      speed: 0,
+      performance: '', // 工作情况
+      kBps: 0,
+      btnDom: null,
+      videoInfo: null,
+      volume: 1,
+      rotate: 0,
+      vod: true, // 点播
+      forceNoOffscreen: false
+    }
+  },
+  watch: {
+    videoUrl: {
+      handler(val, _) {
+        this.$nextTick(() => {
+          this.play(val)
+        })
+      },
+      immediate: true
+    }
+  },
+  created() {
+    const paramUrl = decodeURIComponent(this.$route.params.url)
+    this.$nextTick(() => {
+      this.updatePlayerDomSize()
+      window.onresize = this.updatePlayerDomSize
+      if (typeof (this.videoUrl) === 'undefined') {
+        this.videoUrl = paramUrl
+      }
+      this.btnDom = document.getElementById('buttonsBox')
+    })
+  },
+  // mounted() {
+  //   const ro = new ResizeObserver(entries => {
+  //     entries.forEach(entry => {
+  //       this.updatePlayerDomSize()
+  //     });
+  //   });
+  //   ro.observe(this.$refs.container);
+  // },
+  mounted() {
+    this.updatePlayerDomSize()
+  },
+  destroyed() {
+    if (jessibucaPlayer[this._uid]) {
+      jessibucaPlayer[this._uid].destroy()
+    }
+    this.playing = false
+    this.loaded = false
+    this.performance = ''
+  },
+  methods: {
+    updatePlayerDomSize() {
+      const dom = this.$refs.container
+      if (!dom) return;
+
+      if (!this.parentNodeResizeObserver) {
+        this.parentNodeResizeObserver = new ResizeObserver(entries => {
+          this.updatePlayerDomSize()
+        })
+        this.parentNodeResizeObserver.observe(dom.parentNode)
+      }
+      
+      // 获取父容器尺寸
+      const boxWidth = dom.parentNode.clientWidth
+      const boxHeight = dom.parentNode.clientHeight
+      
+      // 检查是否处于全屏状态
+      const isFullscreen = this.isFullscreen();
+      let width, height;
+      
+      if (isFullscreen) {
+        // 全屏模式,使用窗口尺寸
+        width = window.innerWidth;
+        height = window.innerHeight;
+      } else {
+        // 非全屏模式,使用16:9比例
+        width = boxWidth
+        height = (9 / 16) * width
+        
+        // 如果计算出的高度超过容器高度,则以容器高度为基准重新计算宽度
+        if (boxHeight > 0 && height > boxHeight) {
+          height = boxHeight
+          width = height * 16 / 9
+        }
+      }
+      
+      // 限制尺寸不超过视口
+      const clientHeight = Math.min(document.body.clientHeight, document.documentElement.clientHeight)
+      if (!isFullscreen && height > clientHeight) {
+        height = clientHeight
+        width = (16 / 9) * height
+      }
+      
+      this.playerWidth = width
+      this.playerHeight = height
+      
+      // 应用尺寸到容器
+      dom.style.width = `${width}px`
+      dom.style.height = `${height}px`
+      
+      // 如果播放器存在,更新播放器尺寸
+      if (this.playing && jessibucaPlayer[this._uid]) {
+        jessibucaPlayer[this._uid].resize(width, height)
+      }
+    },
+    
+    // 公共方法:调整大小
+    resize() {
+      this.updatePlayerDomSize()
+    },
+    
+    // 判断是否处于全屏状态
+    isFullscreen() {
+      return document.fullscreenElement ||
+        document.msFullscreenElement ||
+        document.mozFullScreenElement ||
+        document.webkitFullscreenElement || false
+    },
+    create() {
+      const options = {
+        container: this.$refs.container,
+        autoWasm: true,
+        background: '',
+        controlAutoHide: false,
+        debug: false,
+        decoder: 'static/js/jessibuca/decoder.js',
+        forceNoOffscreen: false,
+        hasAudio: typeof (this.hasAudio) === 'undefined' ? true : this.hasAudio,
+        heartTimeout: 5,
+        heartTimeoutReplay: true,
+        heartTimeoutReplayTimes: 3,
+        hiddenAutoPause: false,
+        hotKey: true,
+        isFlv: false,
+        isFullResize: false,
+        isNotMute: this.isNotMute,
+        isResize: true,
+        keepScreenOn: true,
+        loadingText: '请稍等, 视频加载中......',
+        loadingTimeout: 10,
+        loadingTimeoutReplay: true,
+        loadingTimeoutReplayTimes: 3,
+        openWebglAlignment: false,
+        operateBtns: {
+          fullscreen: false,
+          screenshot: false,
+          play: false,
+          audio: false,
+          record: false
+        },
+        recordType: 'mp4',
+        rotate: 0,
+        showBandwidth: false,
+        supportDblclickFullscreen: false,
+        timeout: 10,
+        useMSE: true,
+        useWCS: false,
+        useWebFullScreen: true,
+        videoBuffer: 0.1,
+        wasmDecodeErrorReplay: true,
+        wcsUseVideoRender: true
+      }
+      console.log('Jessibuca -> options: ', options)
+      jessibucaPlayer[this._uid] = new window.Jessibuca({ ...options })
+
+      const jessibuca = jessibucaPlayer[this._uid]
+      const _this = this
+      jessibuca.on('pause', function() {
+        _this.playing = false
+      })
+      jessibuca.on('play', function() {
+        _this.playing = true
+      })
+      jessibuca.on('fullscreen', function(msg) {
+        _this.fullscreen = msg
+      })
+      jessibuca.on('mute', function(msg) {
+        _this.isNotMute = !msg
+      })
+      jessibuca.on('performance', function(performance) {
+        let show = '卡顿'
+        if (performance === 2) {
+          show = '非常流畅'
+        } else if (performance === 1) {
+          show = '流畅'
+        }
+        _this.performance = show
+      })
+      jessibuca.on('kBps', function(kBps) {
+        _this.kBps = Math.round(kBps)
+      })
+      jessibuca.on('videoInfo', function(msg) {
+        console.log('Jessibuca -> videoInfo: ', msg)
+      })
+      jessibuca.on('audioInfo', function(msg) {
+        console.log('Jessibuca -> audioInfo: ', msg)
+      })
+      jessibuca.on('error', function(msg) {
+        console.log('Jessibuca -> error: ', msg)
+      })
+      jessibuca.on('timeout', function(msg) {
+        console.log('Jessibuca -> timeout: ', msg)
+      })
+      jessibuca.on('loadingTimeout', function(msg) {
+        console.log('Jessibuca -> timeout: ', msg)
+      })
+      jessibuca.on('delayTimeout', function(msg) {
+        console.log('Jessibuca -> timeout: ', msg)
+      })
+      jessibuca.on('playToRenderTimes', function(msg) {
+        console.log('Jessibuca -> playToRenderTimes: ', msg)
+      })
+    },
+    playBtnClick: function(event) {
+      this.play(this.videoUrl)
+    },
+    play: function(url) {
+      console.log('Jessibuca -> url: ', url)
+      if (jessibucaPlayer[this._uid]) {
+        this.destroy()
+      }
+      this.create()
+      jessibucaPlayer[this._uid].on('play', () => {
+        this.playing = true
+        this.loaded = true
+        this.quieting = jessibuca.quieting
+      })
+      if (jessibucaPlayer[this._uid].hasLoaded()) {
+        jessibucaPlayer[this._uid].play(url)
+      } else {
+        jessibucaPlayer[this._uid].on('load', () => {
+          jessibucaPlayer[this._uid].play(url)
+        })
+      }
+    },
+    pause: function() {
+      if (jessibucaPlayer[this._uid]) {
+        jessibucaPlayer[this._uid].pause()
+      }
+      this.playing = false
+      this.err = ''
+      this.performance = ''
+    },
+    screenshot: function() {
+      if (jessibucaPlayer[this._uid]) {
+        jessibucaPlayer[this._uid].screenshot()
+      }
+    },
+    mute: function() {
+      if (jessibucaPlayer[this._uid]) {
+        jessibucaPlayer[this._uid].mute()
+      }
+    },
+    cancelMute: function() {
+      if (jessibucaPlayer[this._uid]) {
+        jessibucaPlayer[this._uid].cancelMute()
+      }
+    },
+    destroy: function() {
+      if (jessibucaPlayer[this._uid]) {
+        jessibucaPlayer[this._uid].destroy()
+      }
+      if (document.getElementById('buttonsBox') == null) {
+        this.$refs.container.appendChild(this.btnDom)
+      }
+      jessibucaPlayer[this._uid] = null
+      this.playing = false
+      this.err = ''
+      this.performance = ''
+    },
+    fullscreenSwich: function() {
+      const isFull = this.isFullscreen()
+      
+      if (!isFull) {
+        // 进入全屏
+        try {
+          const container = this.$refs.container;
+          
+          // 尝试使用HTML5全屏API
+          if (container.requestFullscreen) {
+            container.requestFullscreen();
+          } else if (container.webkitRequestFullscreen) {
+            container.webkitRequestFullscreen();
+          } else if (container.msRequestFullscreen) {
+            container.msRequestFullscreen();
+          } else if (container.mozRequestFullScreen) {
+            container.mozRequestFullScreen();
+          } else {
+            // 如果原生API不可用,使用Jessibuca的全屏API
+            if (jessibucaPlayer[this._uid]) {
+              jessibucaPlayer[this._uid].setFullscreen(true);
+            }
+          }
+          
+          // 设置全屏标志
+          this.fullscreen = true;
+        } catch (e) {
+          console.error('全屏切换失败:', e);
+        }
+      } else {
+        // 退出全屏
+        try {
+          if (document.exitFullscreen) {
+            document.exitFullscreen();
+          } else if (document.webkitExitFullscreen) {
+            document.webkitExitFullscreen();
+          } else if (document.msExitFullscreen) {
+            document.msExitFullscreen();
+          } else if (document.mozCancelFullScreen) {
+            document.mozCancelFullScreen();
+          } else {
+            // 如果原生API不可用,使用Jessibuca的全屏API
+            if (jessibucaPlayer[this._uid]) {
+              jessibucaPlayer[this._uid].setFullscreen(false);
+            }
+          }
+          
+          // 设置全屏标志
+          this.fullscreen = false;
+        } catch (e) {
+          console.error('退出全屏失败:', e);
+        }
+      }
+      
+      // 重新计算尺寸
+      setTimeout(() => {
+        this.updatePlayerDomSize();
+      }, 300);
+    }
+  }
+}
+</script>
+
+<style>
+.jessibuca-container {
+  width: 100%; 
+  height: 100%; 
+  background-color: #000000;
+  margin: 0 auto;
+  position: relative;
+  overflow: hidden;
+}
+
+.jessibuca-container.jessibuca-fullscreen {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100vw !important;
+  height: 100vh !important;
+  z-index: 9999;
+}
+
+.jessibuca-player-wrapper {
+  width: 100%; 
+  padding-top: 56.25%; 
+  position: relative;
+}
+
+.buttons-box {
+  width: 100%;
+  height: 28px;
+  background-color: rgba(43, 51, 63, 0.7);
+  position: absolute;
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  left: 0;
+  bottom: 0;
+  user-select: none;
+  z-index: 10;
+}
+
+.jessibuca-btn {
+  width: 20px;
+  color: rgb(255, 255, 255);
+  line-height: 27px;
+  margin: 0px 10px;
+  padding: 0px 2px;
+  cursor: pointer;
+  text-align: center;
+  font-size: 0.8rem !important;
+}
+
+.buttons-box-right {
+  position: absolute;
+  right: 0;
+}
+</style>

+ 302 - 0
components/common/video-player.vue

@@ -0,0 +1,302 @@
+<template>
+  <view class="video-player-container">
+    <!-- H5环境使用Jessibuca -->
+    <view v-if="isH5" class="h5-player">
+      <view ref="jessibucaContainer" class="jessibuca-container"></view>
+      <view v-if="loadError" class="error-message">
+        <text>视频加载失败,请刷新重试</text>
+      </view>
+    </view>
+    
+    <!-- 小程序环境使用live-player -->
+    <view v-else class="mp-player">
+      <live-player 
+        id="videoPlayer"
+        :src="videoUrl" 
+        mode="live"
+        :autoplay="autoplay"
+        :muted="isMuted"
+        object-fit="contain"
+        @statechange="onStateChange"
+        @error="onError"
+        @fullscreenchange="onFullscreenChange"
+        class="live-player"
+      ></live-player>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'VideoPlayer',
+  props: {
+    videoUrl: {
+      type: String,
+      required: true
+    },
+    hasAudio: {
+      type: Boolean,
+      default: true
+    },
+    autoplay: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data() {
+    return {
+      isH5: false,
+      isMuted: false,
+      isPlaying: false,
+      isFullscreen: false,
+      jessibucaPlayer: null,
+      livePlayerContext: null,
+      loadError: false
+    }
+  },
+  mounted() {
+    // 判断当前环境
+    // #ifdef H5
+    this.isH5 = true;
+    this.$nextTick(() => {
+      this.initJessibucaPlayer();
+    });
+    // #endif
+    
+    // #ifdef MP-WEIXIN
+    this.isH5 = false;
+    this.initLivePlayer();
+    // #endif
+  },
+  beforeDestroy() {
+    this.destroyPlayer();
+  },
+  watch: {
+    videoUrl: {
+      handler(newUrl) {
+        if (this.isH5 && this.jessibucaPlayer) {
+          this.destroyPlayer();
+          this.$nextTick(() => {
+            this.initJessibucaPlayer();
+          });
+        }
+      }
+    }
+  },
+  methods: {
+    // 初始化Jessibuca播放器(H5环境)
+    initJessibucaPlayer() {
+      if (!this.isH5 || !this.$jessibuca) {
+        console.error('H5环境或Jessibuca插件未初始化');
+        this.loadError = true;
+        return;
+      }
+      
+      console.log('正在初始化Jessibuca播放器,URL:', this.videoUrl);
+      
+      // 使用Jessibuca插件
+      this.$jessibuca.createPlayer({
+        container: this.$refs.jessibucaContainer,
+        url: this.videoUrl,
+        hasAudio: this.hasAudio,
+        autoplay: this.autoplay,
+        decoder: './static/js/jessibuca/decoder.js',
+        wasmUrl: './static/js/jessibuca/decoder.wasm'
+      }).then(player => {
+        if (!player) {
+          console.error('Jessibuca播放器创建失败');
+          this.loadError = true;
+          return;
+        }
+        
+        this.jessibucaPlayer = player;
+        console.log('Jessibuca播放器创建成功');
+        
+        // 监听事件
+        player.on('play', () => {
+          this.isPlaying = true;
+          this.loadError = false;
+          this.$emit('play');
+        });
+        
+        player.on('pause', () => {
+          this.isPlaying = false;
+          this.$emit('pause');
+        });
+        
+        player.on('error', (err) => {
+          console.error('Jessibuca播放错误:', err);
+          this.loadError = true;
+          this.$emit('error', err);
+        });
+      }).catch(err => {
+        console.error('Jessibuca播放器初始化失败:', err);
+        this.loadError = true;
+      });
+    },
+    
+    // 初始化小程序live-player
+    initLivePlayer() {
+      if (this.isH5) return;
+      
+      // #ifdef MP-WEIXIN
+      this.livePlayerContext = uni.createLivePlayerContext('videoPlayer', this);
+      if (this.autoplay) {
+        this.play();
+      }
+      // #endif
+    },
+    
+    // 播放
+    play() {
+      if (this.isH5 && this.jessibucaPlayer) {
+        this.jessibucaPlayer.play();
+      } else if (this.livePlayerContext) {
+        this.livePlayerContext.play({
+          success: () => {
+            this.isPlaying = true;
+            this.$emit('play');
+          },
+          fail: (err) => {
+            console.error('播放失败:', err);
+            this.$emit('error', err);
+          }
+        });
+      }
+    },
+    
+    // 暂停
+    pause() {
+      if (this.isH5 && this.jessibucaPlayer) {
+        this.jessibucaPlayer.pause();
+      } else if (this.livePlayerContext) {
+        this.livePlayerContext.pause({
+          success: () => {
+            this.isPlaying = false;
+            this.$emit('pause');
+          }
+        });
+      }
+    },
+    
+    // 截图
+    screenshot() {
+      if (this.isH5 && this.jessibucaPlayer) {
+        return this.jessibucaPlayer.screenshot();
+      } else if (this.livePlayerContext) {
+        return new Promise((resolve, reject) => {
+          this.livePlayerContext.snapshot({
+            success: (res) => {
+              resolve(res.tempImagePath);
+            },
+            fail: (err) => {
+              reject(err);
+            }
+          });
+        });
+      }
+    },
+    
+    // 静音
+    mute() {
+      this.isMuted = true;
+      if (this.isH5 && this.jessibucaPlayer) {
+        this.jessibucaPlayer.mute();
+      }
+    },
+    
+    // 取消静音
+    cancelMute() {
+      this.isMuted = false;
+      if (this.isH5 && this.jessibucaPlayer) {
+        this.jessibucaPlayer.cancelMute();
+      }
+    },
+    
+    // 全屏
+    fullscreenSwich() {
+      if (this.isH5 && this.jessibucaPlayer) {
+        this.jessibucaPlayer.fullscreen();
+      } else if (this.livePlayerContext) {
+        if (this.isFullscreen) {
+          this.livePlayerContext.exitFullScreen();
+        } else {
+          this.livePlayerContext.requestFullScreen({
+            direction: 90
+          });
+        }
+      }
+    },
+    
+    // 重置大小
+    resize() {
+      if (this.isH5 && this.jessibucaPlayer) {
+        this.jessibucaPlayer.resize();
+      }
+    },
+    
+    // 检查是否全屏
+    isFullscreen() {
+      return this.isFullscreen;
+    },
+    
+    // live-player状态变化
+    onStateChange(e) {
+      console.log('播放器状态变化:', e.detail);
+      const state = e.detail.code;
+      if (state === 2003) { // 播放中
+        this.isPlaying = true;
+        this.$emit('play');
+      } else if (state === 2004) { // 暂停
+        this.isPlaying = false;
+        this.$emit('pause');
+      }
+    },
+    
+    // live-player错误
+    onError(e) {
+      console.error('播放器错误:', e.detail);
+      this.$emit('error', e.detail);
+    },
+    
+    // 全屏状态变化
+    onFullscreenChange(e) {
+      this.isFullscreen = e.detail.fullScreen;
+      this.$emit('fullscreenchange', this.isFullscreen);
+    },
+    
+    // 销毁播放器
+    destroyPlayer() {
+      if (this.isH5 && this.jessibucaPlayer) {
+        this.jessibucaPlayer.destroy();
+        this.jessibucaPlayer = null;
+      }
+    }
+  }
+}
+</script>
+
+<style>
+.video-player-container {
+  width: 100%;
+  height: 100%;
+  background-color: #000;
+  position: relative;
+}
+
+.h5-player, .mp-player, .jessibuca-container, .live-player {
+  width: 100%;
+  height: 100%;
+}
+
+.error-message {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  color: #fff;
+  background-color: rgba(0, 0, 0, 0.5);
+  padding: 10px 20px;
+  border-radius: 4px;
+}
+</style> 

+ 12 - 1
config/config.js

@@ -23,5 +23,16 @@ export default {
   lightColor: "#ff6b35", // 高亮主题色
   aiderLightColor: "#ff9f28", // 辅助高亮颜色
   defaultUserPhoto: "/static/missing-face.png", // 默认用户头像
-  enableFetchMobileLogin: false // 是否启用获取手机号登录 如果微信小程序提示封禁手机号获取权限 可将此选项设置成false作为备用登录方案
+  enableFetchMobileLogin: false, // 是否启用获取手机号登录 如果微信小程序提示封禁手机号获取权限 可将此选项设置成false作为备用登录方案
+  // 新增视频流服务器配置
+  streamServer: {
+    // RTMP流服务器地址,用于小程序播放
+    rtmpServer: 'rtmp://121.4.16.100:1935',
+    
+    // HLS流服务器地址,用于小程序播放
+    hlsServer: 'http://121.4.16.100:6080/rtp/34020000001110000001_34020000001320000012/hls.m3u8',
+    
+    // WebSocket-FLV流服务器地址,用于H5播放
+    wsFlvServer: 'ws://121.4.16.100:6080/rtp'
+  }
 };

+ 3 - 5
index.html

@@ -1,14 +1,12 @@
 <!DOCTYPE html>
-<html lang="en">
+<html lang="zh-CN">
   <head>
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
-    <title></title>
-    <!--preload-links-->
-    <!--app-context-->
+    <title>农小禹</title>
   </head>
   <body>
-    <div id="app"><!--app-html--></div>
+    <div id="app"></div>
     <script type="module" src="/main.js"></script>
   </body>
 </html>

+ 15 - 4
main.js

@@ -1,10 +1,11 @@
 // main.js(Vue 2)
 import Vue from 'vue'
-import App from './App.vue'
+import App from './App'
 import * as filters from './utils/filters.js'
 import store from './store'
 import uView from 'uview-ui'
 import config from '@/config/config'
+import JessibucaPlugin from './utils/jessibuca-plugin'
 
 // 注册全局过滤器
 Object.keys(filters).forEach((key) => {
@@ -22,9 +23,19 @@ Vue.use(store)
 // 使用 uView UI
 Vue.use(uView)
 
+// 动态加载 Jessibuca 插件,仅限 H5
+// #ifdef H5
+if (typeof window !== 'undefined') {
+  Vue.use(JessibucaPlugin)
+}
+// #endif
+
 Vue.config.productionTip = false
 
-new Vue({
+App.mpType = 'app'
+
+const app = new Vue({
   store,
-  render: h => h(App)
-}).$mount('#app')
+  ...App
+})
+app.$mount() // 必须是这样写,不要在这里指定挂载节点

+ 1 - 1
manifest.json

@@ -18,5 +18,5 @@
             "urlCheck" : false
         }
     },
-    "vueVersion" : "2"
+	"vueVersion": "2"
 }

+ 28 - 24
package-lock.json

@@ -1,26 +1,30 @@
 {
-  "name": "nongxiaoyu_back",
-  "lockfileVersion": 3,
-  "requires": true,
-  "packages": {
-    "": {
-      "dependencies": {
-        "event-source-polyfill": "^1.0.31",
-        "uview-ui": "^2.0.38"
-      }
-    },
-    "node_modules/event-source-polyfill": {
-      "version": "1.0.31",
-      "resolved": "https://registry.npmmirror.com/event-source-polyfill/-/event-source-polyfill-1.0.31.tgz",
-      "integrity": "sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA=="
-    },
-    "node_modules/uview-ui": {
-      "version": "2.0.38",
-      "resolved": "https://registry.npmmirror.com/uview-ui/-/uview-ui-2.0.38.tgz",
-      "integrity": "sha512-6egHDf9lXHKpG3hEjRE0vMx4+VWwKk/ReTf5x18KrIKqdvdPRqO3+B8Unh7vYYwrIxzAWIlmhZ9RJpKI/4UqPQ==",
-      "engines": {
-        "HBuilderX": "^3.1.0"
-      }
-    }
-  }
+	"name": "nongxiaoyu",
+	"version": "1.0.0",
+	"lockfileVersion": 3,
+	"requires": true,
+	"packages": {
+		"": {
+			"name": "nongxiaoyu",
+			"version": "1.0.0",
+			"license": "ISC",
+			"dependencies": {
+				"event-source-polyfill": "^1.0.31",
+				"uview-ui": "^2.0.38"
+			}
+		},
+		"node_modules/event-source-polyfill": {
+			"version": "1.0.31",
+			"resolved": "https://registry.npmmirror.com/event-source-polyfill/-/event-source-polyfill-1.0.31.tgz",
+			"integrity": "sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA=="
+		},
+		"node_modules/uview-ui": {
+			"version": "2.0.38",
+			"resolved": "https://registry.npmmirror.com/uview-ui/-/uview-ui-2.0.38.tgz",
+			"integrity": "sha512-6egHDf9lXHKpG3hEjRE0vMx4+VWwKk/ReTf5x18KrIKqdvdPRqO3+B8Unh7vYYwrIxzAWIlmhZ9RJpKI/4UqPQ==",
+			"engines": {
+				"HBuilderX": "^3.1.0"
+			}
+		}
+	}
 }

+ 27 - 1
package.json

@@ -1,6 +1,32 @@
 {
+	"name": "nongxiaoyu",
+	"version": "1.0.0",
+	"description": "农小禹智慧农业系统",
+	"main": "main.js",
+	"scripts": {
+	  "dev:h5": "cross-env NODE_ENV=development UNI_PLATFORM=h5 vue-cli-service uni-serve",
+	  "dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch",
+	  "build:h5": "cross-env NODE_ENV=production UNI_PLATFORM=h5 vue-cli-service uni-build",
+	  "build:mp-weixin": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin vue-cli-service uni-build"
+	},
+	"keywords": ["农业", "物联网", "智慧农业"],
+	"author": "",
+	"license": "ISC",
   "dependencies": {
     "event-source-polyfill": "^1.0.31",
-    "uview-ui": "^2.0.38"
+    "uview-ui": "^2.0.38",
+    "@dcloudio/uni-app": "^2.0.1",
+    "@dcloudio/uni-h5": "^2.0.1",
+    "@dcloudio/uni-mp-weixin": "^2.0.1",
+    "vue": "^2.6.14",
+    "vuex": "^3.6.2"
+  },
+  "devDependencies": {
+    "@dcloudio/uni-cli-shared": "^2.0.1",
+    "@dcloudio/vue-cli-plugin-uni": "^2.0.1",
+    "@vue/cli-service": "^4.5.15",
+    "cross-env": "^7.0.3",
+    "sass": "^1.49.0",
+    "sass-loader": "^10.2.1"
   }
 }

+ 22 - 5
pages/dashboard/index.vue

@@ -260,12 +260,12 @@
 				columns: [
 					[]
 				],
-				defaultIndex: [9],
+				defaultIndex: [0],
 				// 用户数据
 				userData: {
-					nickname: '',
-					selectedPlot: '',
-					avatar: '',
+					nickname: '游客',
+					selectedPlot: '未选择地块',
+					avatar: '/static/images/user-avatar.png',
 				},
 
 				// 农场数据
@@ -614,7 +614,7 @@
 			// 导航到个人资料
 			navigateToProfile() {
 				uni.navigateTo({
-					url: '/pages/profile/index'
+					url: '/pages/user/index'
 				});
 			},
 
@@ -1572,4 +1572,21 @@
 			}
 		}
 	}
+
+	// 添加测试样式
+	.test-block {
+		background-color: #4CAF50;
+		padding: 30rpx;
+		margin: 20rpx;
+		border-radius: 12rpx;
+		display: flex;
+		justify-content: center;
+		align-items: center;
+	}
+
+	.test-text {
+		color: white;
+		font-size: 36rpx;
+		font-weight: bold;
+	}
 </style>

+ 596 - 117
pages/device/device-list/detail-camera.vue

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

+ 44 - 42
pages/device/device-list/detail-collector.vue

@@ -326,7 +326,7 @@
 
 <script>
 import { getDeviceCollectorDetail } from "@/api/services/device.js";
-
+import { formatSmartTime, formatDate } from '@/utils/dateUtils'
 export default {
   data() {
     return {
@@ -411,6 +411,8 @@ export default {
   },
   
   methods: {
+	formatDate,
+    formatSmartTime,
     // 获取设备采集器详情
     fetchDeviceCollectorDetail() {
       if (!this.deviceInfo.deviceId) return;
@@ -524,36 +526,36 @@ export default {
         });
     },
 	// 格式还日期格式;返回 今天:18:00
-    formatSmartTime(timeStr) {
-      if (!timeStr) return '未知';
+    // formatSmartTime(timeStr) {
+    //   if (!timeStr) return '未知';
     
-      // iOS兼容:将 "2025-06-19 09:15:00" 转换为 "2025-06-19T09:15:00"
-      const safeStr = timeStr.replace(' ', 'T');
+    //   // iOS兼容:将 "2025-06-19 09:15:00" 转换为 "2025-06-19T09:15:00"
+    //   const safeStr = timeStr.replace(' ', 'T');
     
-      const inputDate = new Date(safeStr);
-      if (isNaN(inputDate.getTime())) return '时间格式错误';
+    //   const inputDate = new Date(safeStr);
+    //   if (isNaN(inputDate.getTime())) return '时间格式错误';
     
-      const now = new Date();
+    //   const now = new Date();
     
-      // 取日期差值(单位:天)
-      const inputDayStart = new Date(inputDate.getFullYear(), inputDate.getMonth(), inputDate.getDate());
-      const nowDayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+    //   // 取日期差值(单位:天)
+    //   const inputDayStart = new Date(inputDate.getFullYear(), inputDate.getMonth(), inputDate.getDate());
+    //   const nowDayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
     
-      const diffTime = nowDayStart - inputDayStart;
-      const oneDay = 1000 * 60 * 60 * 24;
+    //   const diffTime = nowDayStart - inputDayStart;
+    //   const oneDay = 1000 * 60 * 60 * 24;
     
-      const timePart = inputDate.toTimeString().slice(0, 5); // HH:mm
+    //   const timePart = inputDate.toTimeString().slice(0, 5); // HH:mm
     
-      if (diffTime === 0) {
-        return `今天 ${timePart}`;
-      } else if (diffTime === oneDay) {
-        return `昨天 ${timePart}`;
-      } else if (diffTime === oneDay * 2) {
-        return `前天 ${timePart}`;
-      } else {
-        return `${inputDate.getMonth() + 1}月${inputDate.getDate()}日 ${timePart}`;
-      }
-    },
+    //   if (diffTime === 0) {
+    //     return `今天 ${timePart}`;
+    //   } else if (diffTime === oneDay) {
+    //     return `昨天 ${timePart}`;
+    //   } else if (diffTime === oneDay * 2) {
+    //     return `前天 ${timePart}`;
+    //   } else {
+    //     return `${inputDate.getMonth() + 1}月${inputDate.getDate()}日 ${timePart}`;
+    //   }
+    // },
 
     // 刷新数据
     refreshData() {
@@ -563,30 +565,30 @@ export default {
       this.fetchDeviceCollectorDetail();
     },
 	// 格式化日期
-	formatDate(dateStr) {
-	  if (!dateStr) return '未知';
+	// formatDate(dateStr) {
+	//   if (!dateStr) return '未知';
 	  
-	  // 解析为 Date
-	  const parsedStr = dateStr.replace(' ', 'T'); 
-	  const date = new Date(parsedStr);
-	  if (isNaN(date)) return '无效时间';
+	//   // 解析为 Date
+	//   const parsedStr = dateStr.replace(' ', 'T'); 
+	//   const date = new Date(parsedStr);
+	//   if (isNaN(date)) return '无效时间';
 	  
-	  const now = new Date();
+	//   const now = new Date();
 	
-	  // 计算差时长(分钟)
-	  const diff = Math.floor((now - date) / 1000 / 60);
+	//   // 计算差时长(分钟)
+	//   const diff = Math.floor((now - date) / 1000 / 60);
 	  
-	  if (diff < 1) return '刚刚更新';
-	  if (diff < 5) return '1分钟前更新';
-	  if (diff < 10) return '5分钟前更新';
-	  if (diff < 60) return `${diff}分钟前更新`;
+	//   if (diff < 1) return '刚刚更新';
+	//   if (diff < 5) return '1分钟前更新';
+	//   if (diff < 10) return '5分钟前更新';
+	//   if (diff < 60) return `${diff}分钟前更新`;
 	  
-	  if (diff < 120) return '1小时前更新';
-	  if (diff < 24 * 60) return `${Math.floor(diff / 60)}小时前更新`;
-	  if (diff < 7 * 24 * 60) return `${Math.floor(diff / (60 * 24))}天前更新`;
+	//   if (diff < 120) return '1小时前更新';
+	//   if (diff < 24 * 60) return `${Math.floor(diff / 60)}小时前更新`;
+	//   if (diff < 7 * 24 * 60) return `${Math.floor(diff / (60 * 24))}天前更新`;
 	  
-	  return parsedStr.split('T')[0] + ' 更新';
-	},
+	//   return parsedStr.split('T')[0] + ' 更新';
+	// },
   }
 }
 </script>

+ 1 - 1
pages/user/index.vue

@@ -76,7 +76,7 @@
 					</view>
 					<text class="arrow">></text>
 				</view>
-				<view v-if="this.isLogin" class="function-item" @click="handleLogout">
+				<view v-if="isLogin" class="function-item" @click="handleLogout">
 					<view class="left">
 						<text class="function-icon">退</text>
 						<text>退出登录</text>

BIN
static/icons/ai.png


BIN
static/icons/chat.png


BIN
static/icons/chat_off.png


BIN
static/icons/device_alert.png


BIN
static/icons/device_num.png


BIN
static/icons/device_offline.png


BIN
static/icons/device_online.png


BIN
static/icons/user_icon.png


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
static/js/jessibuca/decoder.js


BIN
static/js/jessibuca/decoder.wasm


+ 637 - 0
static/js/jessibuca/jessibuca.d.ts

@@ -0,0 +1,637 @@
+declare namespace Jessibuca {
+
+    /** 超时信息 */
+    enum TIMEOUT {
+        /** 当play()的时候,如果没有数据返回 */
+        loadingTimeout = 'loadingTimeout',
+        /** 当播放过程中,如果超过timeout之后没有数据渲染 */
+        delayTimeout = 'delayTimeout',
+    }
+
+    /** 错误信息 */
+    enum ERROR {
+        /** 播放错误,url 为空的时候,调用 play 方法 */
+        playError = 'playError',
+        /** http 请求失败 */
+        fetchError = 'fetchError',
+        /** websocket 请求失败 */
+        websocketError = 'websocketError',
+        /** webcodecs 解码 h265 失败 */
+        webcodecsH265NotSupport = 'webcodecsH265NotSupport',
+        /** mediaSource 解码 h265 失败 */
+        mediaSourceH265NotSupport = 'mediaSourceH265NotSupport',
+        /** wasm 解码失败 */
+        wasmDecodeError = 'wasmDecodeError',
+    }
+
+    interface Config {
+        /**
+         * 播放器容器
+         * *  若为 string ,则底层调用的是 document.getElementById('id')
+         * */
+        container: HTMLElement | string;
+        /**
+         * 设置最大缓冲时长,单位秒,播放器会自动消除延迟
+         */
+        videoBuffer?: number;
+        /**
+         * worker地址
+         * *  默认引用的是根目录下面的decoder.js文件 ,decoder.js 与 decoder.wasm文件必须是放在同一个目录下面。 */
+        decoder?: string;
+        /**
+         * 是否不使用离屏模式(提升渲染能力)
+         */
+        forceNoOffscreen?: boolean;
+        /**
+         * 是否开启当页面的'visibilityState'变为'hidden'的时候,自动暂停播放。
+         */
+        hiddenAutoPause?: boolean;
+        /**
+         * 是否有音频,如果设置`false`,则不对音频数据解码,提升性能。
+         */
+        hasAudio?: boolean;
+        /**
+         * 设置旋转角度,只支持,0(默认),180,270 三个值
+         */
+        rotate?: boolean;
+        /**
+         * 1. 当为`true`的时候:视频画面做等比缩放后,高或宽对齐canvas区域,画面不被拉伸,但有黑边。 等同于 `setScaleMode(1)`
+         * 2. 当为`false`的时候:视频画面完全填充canvas区域,画面会被拉伸。等同于 `setScaleMode(0)`
+         */
+        isResize?: boolean;
+        /**
+         * 1. 当为`true`的时候:视频画面做等比缩放后,完全填充canvas区域,画面不被拉伸,没有黑边,但画面显示不全。等同于 `setScaleMode(2)`
+         */
+        isFullResize?: boolean;
+        /**
+         * 1. 当为`true`的时候:ws协议不检验是否以.flv为依据,进行协议解析。
+         */
+        isFlv?: boolean;
+        /**
+         * 是否开启控制台调试打
+         */
+        debug?: boolean;
+        /**
+         * 1. 设置超时时长, 单位秒
+         * 2. 在连接成功之前(loading)和播放中途(heart),如果超过设定时长无数据返回,则回调timeout事件
+         */
+        timeout?: number;
+        /**
+         * 1. 设置超时时长, 单位秒
+         * 2. 在连接成功之前,如果超过设定时长无数据返回,则回调timeout事件
+         */
+        heartTimeout?: number;
+        /**
+         * 1. 设置超时时长, 单位秒
+         * 2. 在连接成功之前,如果超过设定时长无数据返回,则回调timeout事件
+         */
+        loadingTimeout?: number;
+        /**
+         * 是否支持屏幕的双击事件,触发全屏,取消全屏事件
+         */
+        supportDblclickFullscreen?: boolean;
+        /**
+         * 是否显示网
+         */
+        showBandwidth?: boolean;
+        /**
+         * 配置操作按钮
+         */
+        operateBtns?: {
+            /** 是否显示全屏按钮 */
+            fullscreen?: boolean;
+            /** 是否显示截图按钮 */
+            screenshot?: boolean;
+            /** 是否显示播放暂停按钮 */
+            play?: boolean;
+            /** 是否显示声音按钮 */
+            audio?: boolean;
+            /** 是否显示录制按 */
+            record?: boolean;
+        };
+        /**
+         * 开启屏幕常亮,在手机浏览器上, canvas标签渲染视频并不会像video标签那样保持屏幕常亮
+         */
+        keepScreenOn?: boolean;
+        /**
+         * 是否开启声音,默认是关闭声音播放的
+         */
+        isNotMute?: boolean;
+        /**
+         * 加载过程中文案
+         */
+        loadingText?: string;
+        /**
+         * 背景图片
+         */
+        background?: string;
+        /**
+         * 是否开启MediaSource硬解码
+         * * 视频编码只支持H.264视频(Safari on iOS不支持)
+         * * 不支持 forceNoOffscreen 为 false (开启离屏渲染)
+         */
+        useMSE?: boolean;
+        /**
+         * 是否开启Webcodecs硬解码
+         * *  视频编码只支持H.264视频 (需在chrome 94版本以上,需要https或者localhost环境)
+         * *  支持 forceNoOffscreen 为 false (开启离屏渲染)
+         * */
+        useWCS?: boolean;
+        /**
+         * 是否开启键盘快捷键
+         * 目前支持的键盘快捷键有:esc -> 退出全屏;arrowUp -> 声音增加;arrowDown -> 声音减少;
+         */
+        hotKey?: boolean;
+        /**
+         *  在使用MSE或者Webcodecs 播放H265的时候,是否自动降级到wasm模式。
+         *  设置为false 则直接关闭播放,抛出Error 异常,设置为true 则会自动切换成wasm模式播放。
+         */
+        autoWasm?: boolean;
+        /**
+         * heartTimeout 心跳超时之后自动再播放,不再抛出异常,而直接重新播放视频地址。
+         */
+        heartTimeoutReplay?: boolean,
+        /**
+         * heartTimeoutReplay 从试次数,超过之后,不再自动播放
+         */
+        heartTimeoutReplayTimes?: number,
+        /**
+         * loadingTimeout loading之后自动再播放,不再抛出异常,而直接重新播放视频地址。
+         */
+        loadingTimeoutReplay?: boolean,
+        /**
+         * heartTimeoutReplay 从试次数,超过之后,不再自动播放
+         */
+        loadingTimeoutReplayTimes?: number
+        /**
+         * wasm解码报错之后,不再抛出异常,而是直接重新播放视频地址。
+         */
+        wasmDecodeErrorReplay?: boolean,
+        /**
+         * https://github.com/langhuihui/jessibuca/issues/152 解决方案
+         * 例如:WebGL图像预处理默认每次取4字节的数据,但是540x960分辨率下的U、V分量宽度是540/2=270不能被4整除,导致绿屏。
+         */
+        openWebglAlignment?: boolean
+    }
+}
+
+
+declare class Jessibuca {
+
+    constructor(config?: Jessibuca.Config);
+
+    /**
+     * 是否开启控制台调试打印
+     @example
+     // 开启
+     jessibuca.setDebug(true)
+     // 关闭
+     jessibuca.setDebug(false)
+     */
+    setDebug(flag: boolean): void;
+
+    /**
+     * 静音
+     @example
+     jessibuca.mute()
+     */
+    mute(): void;
+
+    /**
+     * 取消静音
+     @example
+     jessibuca.cancelMute()
+     */
+    cancelMute(): void;
+
+    /**
+     * 留给上层用户操作来触发音频恢复的方法。
+     *
+     * iPhone,chrome等要求自动播放时,音频必须静音,需要由一个真实的用户交互操作来恢复,不能使用代码。
+     *
+     * https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
+     */
+    audioResume(): void;
+
+    /**
+     *
+     * 设置超时时长, 单位秒
+     * 在连接成功之前和播放中途,如果超过设定时长无数据返回,则回调timeout事件
+
+     @example
+     jessibuca.setTimeout(10)
+
+     jessibuca.on('timeout',function(){
+        //
+    });
+     */
+    setTimeout(): void;
+
+    /**
+     * @param mode
+     *      0 视频画面完全填充canvas区域,画面会被拉伸  等同于参数 `isResize` 为false
+     *
+     *      1 视频画面做等比缩放后,高或宽对齐canvas区域,画面不被拉伸,但有黑边 等同于参数 `isResize` 为true
+     *
+     *      2 视频画面做等比缩放后,完全填充canvas区域,画面不被拉伸,没有黑边,但画面显示不全 等同于参数 `isFullResize` 为true
+     @example
+     jessibuca.setScaleMode(0)
+
+     jessibuca.setScaleMode(1)
+
+     jessibuca.setScaleMode(2)
+     */
+    setScaleMode(mode: number): void;
+
+    /**
+     * 暂停播放
+     *
+     * 可以在pause 之后,再调用 `play()`方法就继续播放之前的流。
+     @example
+     jessibuca.pause().then(()=>{
+        console.log('pause success')
+
+        jessibuca.play().then(()=>{
+
+        }).catch((e)=>{
+
+        })
+
+    }).catch((e)=>{
+        console.log('pause error',e);
+    })
+     */
+    pause(): Promise<void>;
+
+    /**
+     * 关闭视频,不释放底层资源
+     @example
+     jessibuca.close();
+     */
+    close(): void;
+
+    /**
+     * 关闭视频,释放底层资源
+     @example
+     jessibuca.destroy()
+     */
+    destroy(): void;
+
+    /**
+     * 清理画布为黑色背景
+     @example
+     jessibuca.clearView()
+     */
+    clearView(): void;
+
+    /**
+     * 播放视频
+     @example
+
+     jessibuca.play('url').then(()=>{
+        console.log('play success')
+    }).catch((e)=>{
+        console.log('play error',e)
+    })
+     //
+     jessibuca.play()
+     */
+    play(url?: string): Promise<void>;
+
+    /**
+     * 重新调整视图大小
+     */
+    resize(): void;
+
+    /**
+     * 设置最大缓冲时长,单位秒,播放器会自动消除延迟。
+     *
+     * 等同于 `videoBuffer` 参数。
+     *
+     @example
+     // 设置 200ms 缓冲
+     jessibuca.setBufferTime(0.2)
+     */
+    setBufferTime(time: number): void;
+
+    /**
+     * 设置旋转角度,只支持,0(默认) ,180,270 三个值。
+     *
+     * > 可用于实现监控画面小窗和全屏效果,由于iOS没有全屏API,此方法可以模拟页面内全屏效果而且多端效果一致。   *
+     @example
+     jessibuca.setRotate(0)
+
+     jessibuca.setRotate(90)
+
+     jessibuca.setRotate(270)
+     */
+    setRotate(deg: number): void;
+
+    /**
+     *
+     * 设置音量大小,取值0 — 1
+     *
+     * > 区别于 mute 和 cancelMute 方法,虽然设置setVolume(0) 也能达到 mute方法,但是mute 方法是不调用底层播放音频的,能提高性能。而setVolume(0)只是把声音设置为0 ,以达到效果。
+     * @param volume 当为0时,完全无声;当为1时,最大音量,默认值
+     @example
+     jessibuca.setVolume(0.2)
+
+     jessibuca.setVolume(0)
+
+     jessibuca.setVolume(1)
+     */
+    setVolume(volume: number): void;
+
+    /**
+     * 返回是否加载完毕
+     @example
+     var result = jessibuca.hasLoaded()
+     console.log(result) // true
+     */
+    hasLoaded(): boolean;
+
+    /**
+     * 开启屏幕常亮,在手机浏览器上, canvas标签渲染视频并不会像video标签那样保持屏幕常亮。
+     * H5目前在chrome\edge 84, android chrome 84及以上有原生亮屏API, 需要是https页面
+     * 其余平台为模拟实现,此时为兼容实现,并不保证所有浏览器都支持
+     @example
+     jessibuca.setKeepScreenOn()
+     */
+    setKeepScreenOn(): boolean;
+
+    /**
+     * 全屏(取消全屏)播放视频
+     @example
+     jessibuca.setFullscreen(true)
+     //
+     jessibuca.setFullscreen(false)
+     */
+    setFullscreen(flag: boolean): void;
+
+    /**
+     *
+     * 截图,调用后弹出下载框保存截图
+     * @param filename 可选参数, 保存的文件名, 默认 `时间戳`
+     * @param format   可选参数, 截图的格式,可选png或jpeg或者webp ,默认 `png`
+     * @param quality  可选参数, 当格式是jpeg或者webp时,压缩质量,取值0 ~ 1 ,默认 `0.92`
+     * @param type 可选参数, 可选download或者base64或者blob,默认`download`
+
+     @example
+
+     jessibuca.screenshot("test","png",0.5)
+
+     const base64 = jessibuca.screenshot("test","png",0.5,'base64')
+
+     const fileBlob = jessibuca.screenshot("test",'blob')
+     */
+    screenshot(filename?: string, format?: string, quality?: number, type?: string): void;
+
+    /**
+     * 开始录制。
+     * @param fileName 可选,默认时间戳
+     * @param fileType 可选,默认webm,支持webm 和mp4 格式
+
+     @example
+     jessibuca.startRecord('xxx','webm')
+     */
+    startRecord(fileName: string, fileType: string): void;
+
+    /**
+     * 暂停录制并下载。
+     @example
+     jessibuca.stopRecordAndSave()
+     */
+    stopRecordAndSave(): void;
+
+    /**
+     * 返回是否正在播放中状态。
+     @example
+     var result = jessibuca.isPlaying()
+     console.log(result) // true
+     */
+    isPlaying(): boolean;
+
+    /**
+     *   返回是否静音。
+     @example
+     var result = jessibuca.isMute()
+     console.log(result) // true
+     */
+    isMute(): boolean;
+
+    /**
+     * 返回是否正在录制。
+     @example
+     var result = jessibuca.isRecording()
+     console.log(result) // true
+     */
+    isRecording(): boolean;
+
+
+    /**
+     * 监听 jessibuca 初始化事件
+     * @example
+     * jessibuca.on("load",function(){console.log('load')})
+     */
+    on(event: 'load', callback: () => void): void;
+
+    /**
+     * 视频播放持续时间,单位ms
+     * @example
+     * jessibuca.on('timeUpdate',function (ts) {console.log('timeUpdate',ts);})
+     */
+    on(event: 'timeUpdate', callback: () => void): void;
+
+    /**
+     * 当解析出视频信息时回调,2个回调参数
+     * @example
+     * jessibuca.on("videoInfo",function(data){console.log('width:',data.width,'height:',data.width)})
+     */
+    on(event: 'videoInfo', callback: (data: {
+        /** 视频宽 */
+        width: number;
+        /** 视频高 */
+        height: number;
+    }) => void): void;
+
+    /**
+     * 当解析出音频信息时回调,2个回调参数
+     * @example
+     * jessibuca.on("audioInfo",function(data){console.log('numOfChannels:',data.numOfChannels,'sampleRate',data.sampleRate)})
+     */
+    on(event: 'audioInfo', callback: (data: {
+        /** 声频通道 */
+        numOfChannels: number;
+        /** 采样率 */
+        sampleRate: number;
+    }) => void): void;
+
+    /**
+     * 信息,包含错误信息
+     * @example
+     * jessibuca.on("log",function(data){console.log('data:',data)})
+     */
+    on(event: 'log', callback: () => void): void;
+
+    /**
+     * 错误信息
+     * @example
+     * jessibuca.on("error",function(error){
+        if(error === Jessibuca.ERROR.fetchError){
+            //
+        }
+        else if(error === Jessibuca.ERROR.webcodecsH265NotSupport){
+            //
+        }
+        console.log('error:',error)
+    })
+     */
+    on(event: 'error', callback: (err: Jessibuca.ERROR) => void): void;
+
+    /**
+     * 当前网速, 单位KB 每秒1次,
+     * @example
+     * jessibuca.on("kBps",function(data){console.log('kBps:',data)})
+     */
+    on(event: 'kBps', callback: (value: number) => void): void;
+
+    /**
+     * 渲染开始
+     * @example
+     * jessibuca.on("start",function(){console.log('start render')})
+     */
+    on(event: 'start', callback: () => void): void;
+
+    /**
+     * 当设定的超时时间内无数据返回,则回调
+     * @example
+     * jessibuca.on("timeout",function(error){console.log('timeout:',error)})
+     */
+    on(event: 'timeout', callback: (error: Jessibuca.TIMEOUT) => void): void;
+
+    /**
+     * 当play()的时候,如果没有数据返回,则回调
+     * @example
+     * jessibuca.on("loadingTimeout",function(){console.log('timeout')})
+     */
+    on(event: 'loadingTimeout', callback: () => void): void;
+
+    /**
+     * 当播放过程中,如果超过timeout之后没有数据渲染,则抛出异常。
+     * @example
+     * jessibuca.on("delayTimeout",function(){console.log('timeout')})
+     */
+    on(event: 'delayTimeout', callback: () => void): void;
+
+    /**
+     * 当前是否全屏
+     * @example
+     * jessibuca.on("fullscreen",function(flag){console.log('is fullscreen',flag)})
+     */
+    on(event: 'fullscreen', callback: () => void): void;
+
+    /**
+     * 触发播放事件
+     * @example
+     * jessibuca.on("play",function(flag){console.log('play')})
+     */
+    on(event: 'play', callback: () => void): void;
+
+    /**
+     * 触发暂停事件
+     * @example
+     * jessibuca.on("pause",function(flag){console.log('pause')})
+     */
+    on(event: 'pause', callback: () => void): void;
+
+    /**
+     * 触发声音事件,返回boolean值
+     * @example
+     * jessibuca.on("mute",function(flag){console.log('is mute',flag)})
+     */
+    on(event: 'mute', callback: () => void): void;
+
+    /**
+     * 流状态统计,流开始播放后回调,每秒1次。
+     * @example
+     * jessibuca.on("stats",function(s){console.log("stats is",s)})
+     */
+    on(event: 'stats', callback: (stats: {
+        /** 当前缓冲区时长,单位毫秒 */
+        buf: number;
+        /** 当前视频帧率 */
+        fps: number;
+        /** 当前音频码率,单位byte */
+        abps: number;
+        /** 当前视频码率,单位byte */
+        vbps: number;
+        /** 当前视频帧pts,单位毫秒 */
+        ts: number;
+    }) => void): void;
+
+    /**
+     * 渲染性能统计,流开始播放后回调,每秒1次。
+     * @param performance 0: 表示卡顿,1: 表示流畅,2: 表示非常流程
+     * @example
+     * jessibuca.on("performance",function(performance){console.log("performance is",performance)})
+     */
+    on(event: 'performance', callback: (performance: 0 | 1 | 2) => void): void;
+
+    /**
+     * 录制开始的事件
+
+     * @example
+     * jessibuca.on("recordStart",function(){console.log("record start")})
+     */
+    on(event: 'recordStart', callback: () => void): void;
+
+    /**
+     * 录制结束的事件
+
+     * @example
+     * jessibuca.on("recordEnd",function(){console.log("record end")})
+     */
+    on(event: 'recordEnd', callback: () => void): void;
+
+    /**
+     * 录制的时候,返回的录制时长,1s一次
+
+     * @example
+     * jessibuca.on("recordingTimestamp",function(timestamp){console.log("recordingTimestamp is",timestamp)})
+     */
+    on(event: 'recordingTimestamp', callback: (timestamp: number) => void): void;
+
+    /**
+     * 监听调用play方法 经过 初始化-> 网络请求-> 解封装 -> 解码 -> 渲染 一系列过程的时间消耗
+     * @param event
+     * @param callback
+     */
+    on(event: 'playToRenderTimes', callback: (times: {
+        playInitStart: number, // 1 初始化
+        playStart: number, // 2 初始化
+        streamStart: number, // 3 网络请求
+        streamResponse: number, // 4 网络请求
+        demuxStart: number, // 5 解封装
+        decodeStart: number, // 6 解码
+        videoStart: number, // 7 渲染
+        playTimestamp: number,// playStart- playInitStart
+        streamTimestamp: number,// streamStart - playStart
+        streamResponseTimestamp: number,// streamResponse - streamStart
+        demuxTimestamp: number, // demuxStart - streamResponse
+        decodeTimestamp: number, // decodeStart - demuxStart
+        videoTimestamp: number,// videoStart - decodeStart
+        allTimestamp: number // videoStart - playInitStart
+    }) => void): void
+
+    /**
+     * 监听方法
+     *
+     @example
+
+     jessibuca.on("load",function(){console.log('load')})
+     */
+    on(event: string, callback: Function): void;
+
+}
+
+export default Jessibuca;

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
static/js/jessibuca/jessibuca.js


+ 72 - 0
utils/dateUtils.js

@@ -0,0 +1,72 @@
+// utils/dateUtils.js
+
+/**
+ * 智能时间格式化:返回 "今天 18:00"、"昨天 18:00" 等
+ */
+export function formatSmartTime(timeStr) {
+  if (!timeStr) return '未知';
+
+  // iOS兼容格式
+  const safeStr = timeStr.replace(' ', 'T');
+  const inputDate = new Date(safeStr);
+  if (isNaN(inputDate.getTime())) return '时间格式错误';
+
+  const now = new Date();
+  const inputDayStart = new Date(inputDate.getFullYear(), inputDate.getMonth(), inputDate.getDate());
+  const nowDayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+
+  const diffTime = nowDayStart - inputDayStart;
+  const oneDay = 1000 * 60 * 60 * 24;
+
+  const timePart = inputDate.toTimeString().slice(0, 5); // HH:mm
+
+  if (diffTime === 0) {
+    return `今天 ${timePart}`;
+  } else if (diffTime === oneDay) {
+    return `昨天 ${timePart}`;
+  } else if (diffTime === oneDay * 2) {
+    return `前天 ${timePart}`;
+  } else {
+    return `${inputDate.getMonth() + 1}月${inputDate.getDate()}日 ${timePart}`;
+  }
+}
+
+/**
+ * 相对时间格式化:返回 "刚刚更新"、"5分钟前更新"、"3天前更新" 等
+ */
+export function formatDate(dateStr) {
+  if (!dateStr) return '未知';
+
+  const parsedStr = dateStr.replace(' ', 'T');
+  const date = new Date(parsedStr);
+  if (isNaN(date.getTime())) return '无效时间';
+
+  const now = new Date();
+  const diff = Math.floor((now - date) / 1000 / 60); // 单位:分钟
+
+  if (diff < 1) return '刚刚更新';
+  if (diff < 5) return '1分钟前更新';
+  if (diff < 10) return '5分钟前更新';
+  if (diff < 60) return `${diff}分钟前更新`;
+
+  if (diff < 120) return '1小时前更新';
+  if (diff < 24 * 60) return `${Math.floor(diff / 60)}小时前更新`;
+  if (diff < 7 * 24 * 60) return `${Math.floor(diff / (60 * 24))}天前更新`;
+
+  return parsedStr.split('T')[0] + ' 更新';
+}
+
+export function getFormattedTime() {
+  const now = new Date();
+  const pad = n => n < 10 ? '0' + n : n;
+
+  const year = now.getFullYear();
+  const month = pad(now.getMonth() + 1);
+  const day = pad(now.getDate());
+
+  const hours = pad(now.getHours());
+  const minutes = pad(now.getMinutes());
+  const seconds = pad(now.getSeconds());
+
+  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+}

+ 85 - 0
utils/jessibuca-plugin.js

@@ -0,0 +1,85 @@
+/**
+ * Jessibuca视频播放器插件
+ * 用于初始化Jessibuca所需的脚本和依赖
+ */
+
+function loadJessibucaScript() {
+  return new Promise((resolve, reject) => {
+    if (typeof window === 'undefined') {
+      return reject(new Error('非浏览器环境,无法加载Jessibuca'))
+    }
+
+    if (window.Jessibuca) {
+      return resolve(window.Jessibuca)
+    }
+
+    const script = document.createElement('script')
+    script.src = './static/js/jessibuca/jessibuca.js' // 修改路径为相对路径
+    script.async = true
+
+    script.onload = () => {
+      console.log('Jessibuca脚本加载成功')
+      resolve(window.Jessibuca)
+    }
+
+    script.onerror = (error) => {
+      console.error('Jessibuca脚本加载失败:', error)
+      // 尝试使用备用路径
+      const backupScript = document.createElement('script')
+      backupScript.src = '/static/js/jessibuca/jessibuca.js' 
+      backupScript.async = true
+      
+      backupScript.onload = () => {
+        console.log('Jessibuca备用脚本加载成功')
+        resolve(window.Jessibuca)
+      }
+      
+      backupScript.onerror = (backupError) => {
+        console.error('Jessibuca备用脚本加载失败:', backupError)
+        reject(backupError)
+      }
+      
+      document.head.appendChild(backupScript)
+    }
+
+    document.head.appendChild(script)
+  })
+}
+
+// 判断是否支持 Jessibuca(H5环境)
+function isSupported() {
+  return typeof window !== 'undefined' && typeof document !== 'undefined'
+}
+
+export default {
+  install(Vue) {
+    Vue.prototype.$jessibuca = {
+      loadScript: loadJessibucaScript,
+      createPlayer(options) {
+        return loadJessibucaScript().then((Jessibuca) => {
+          if (!Jessibuca) {
+            console.log('Jessibuca 加载失败,无法创建播放器')
+            return null
+          }
+          
+          // 确保设置了decoder和wasm路径
+          options = options || {};
+          options.decoder = options.decoder || './static/js/jessibuca/decoder.js';
+          options.wasmUrl = options.wasmUrl || './static/js/jessibuca/decoder.wasm';
+          
+          return new Jessibuca(options)
+        })
+      },
+      isSupported
+    }
+
+    // 预加载脚本(H5环境下执行)
+    if (isSupported()) {
+      setTimeout(() => {
+        loadJessibucaScript().catch(err => {
+          console.error('预加载 Jessibuca 失败:', err)
+        })
+      }, 1000)
+    }
+  }
+}

+ 113 - 0
utils/media-utils.js

@@ -0,0 +1,113 @@
+/**
+ * 媒体工具类 - 处理视频流地址转换
+ */
+
+// 流协议类型
+export const STREAM_PROTOCOLS = {
+  RTMP: 'rtmp://',
+  HTTP_FLV: ['http://', 'https://'],
+  HLS: ['http://', 'https://'],
+  WS_FLV: ['ws://', 'wss://']
+}
+
+/**
+ * 判断视频流类型
+ * @param {String} url 视频流地址
+ * @returns {String} 流类型,如 'rtmp', 'http-flv', 'hls', 'ws-flv'
+ */
+export const getStreamType = (url) => {
+  if (!url) return null
+  
+  const lowerUrl = url.toLowerCase()
+  
+  if (lowerUrl.startsWith(STREAM_PROTOCOLS.RTMP)) {
+    return 'rtmp'
+  }
+  
+  if (STREAM_PROTOCOLS.HTTP_FLV.some(protocol => lowerUrl.startsWith(protocol)) && lowerUrl.endsWith('.flv')) {
+    return 'http-flv'
+  }
+  
+  if (STREAM_PROTOCOLS.HLS.some(protocol => lowerUrl.startsWith(protocol)) && 
+      (lowerUrl.endsWith('.m3u8') || lowerUrl.includes('.m3u8?'))) {
+    return 'hls'
+  }
+  
+  if (STREAM_PROTOCOLS.WS_FLV.some(protocol => lowerUrl.startsWith(protocol)) && 
+      (lowerUrl.endsWith('.flv') || lowerUrl.includes('.flv?') || lowerUrl.includes('live.flv'))) {
+    return 'ws-flv'
+  }
+  
+  return 'unknown'
+}
+
+/**
+ * 检查流是否在小程序中可播放
+ * @param {String} url 视频流地址
+ * @returns {Boolean} 是否可在小程序中播放
+ */
+export const isPlayableInMiniProgram = (url) => {
+  const streamType = getStreamType(url)
+  // 小程序只支持RTMP和HLS(m3u8)
+  return streamType === 'rtmp' || streamType === 'hls'
+}
+
+/**
+ * 构建适用于不同平台的流地址
+ * @param {String} originalUrl 原始流地址
+ * @param {Object} options 配置选项
+ * @returns {Object} 不同平台适用的流地址
+ */
+export const buildPlatformStreamUrls = (originalUrl, options = {}) => {
+  // 提取流的关键信息 (比如设备ID)
+  const streamType = getStreamType(originalUrl)
+  const { streamServer = {}, fallbackToHls = true } = options
+  
+  // 获取流服务器地址
+  const { rtmpServer, hlsServer, wsFlvServer } = streamServer
+  
+  // 提取设备ID或流标识 (示例: 使用正则表达式提取)
+  const streamIdMatch = originalUrl.match(/(\d+_\d+\.live\.flv)/) || 
+                         originalUrl.match(/([^/]+)\.m3u8/) ||
+                         originalUrl.match(/stream=([^&]+)/)
+  
+  const streamId = streamIdMatch ? streamIdMatch[1] : null
+  
+  if (!streamId) {
+    // 如果无法提取流ID,则返回原始URL
+    return {
+      h5Url: originalUrl,
+      miniProgramUrl: isPlayableInMiniProgram(originalUrl) ? originalUrl : null
+    }
+  }
+  
+  // 构建不同平台的URL
+  const urls = {
+    h5Url: originalUrl, // 默认H5使用原始URL
+    miniProgramUrl: null
+  }
+  
+  // 微信小程序优先使用RTMP和HLS
+  if (rtmpServer && streamType !== 'rtmp') {
+    // 构建RTMP URL (小程序支持)
+    urls.miniProgramUrl = `${rtmpServer}/${streamId.replace('.live.flv', '')}`
+  } else if (hlsServer && streamType !== 'hls') {
+    // 构建HLS URL (小程序支持)
+    urls.miniProgramUrl = `${hlsServer}/${streamId.replace('.live.flv', '.m3u8')}`
+  } else if (isPlayableInMiniProgram(originalUrl)) {
+    // 原URL已经是小程序可播放格式
+    urls.miniProgramUrl = originalUrl
+  } else if (fallbackToHls) {
+    // 假设存在通用HLS地址可以使用
+    const deviceId = streamId.split('_')[0] || streamId
+    urls.miniProgramUrl = `${hlsServer || 'https://stream.example.com/hls'}/${deviceId}.m3u8`
+  }
+  
+  return urls
+}
+
+export default {
+  getStreamType,
+  isPlayableInMiniProgram,
+  buildPlatformStreamUrls
+} 

+ 4 - 0
utils/storage.js

@@ -153,4 +153,8 @@ export default {
 	getImGoodsLink() {
 		return uni.getStorageSync('imGoodId');
 	},
+	// 是否登录检查
+	isLoggedIn() {
+		return !!this.getHasLogin() && !!this.getAccessToken();
+	},
 };

Някои файлове не бяха показани, защото твърде много файлове са промени