LiveVideoFlv.vue 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. <template>
  2. <div class="videoWrapper">
  3. <video ref="videoRef" autoplay muted class="video-js video-content" :poster="poster" disablePictureInPicture>
  4. <source :src="urlWithToken" />
  5. </video>
  6. <img class="loading" :src="loadingImg" v-show="loading" />
  7. </div>
  8. </template>
  9. <script setup lang="ts">
  10. import { onMounted, onBeforeUnmount, watch, ref, computed } from 'vue';
  11. import mpegts from 'mpegts.js';
  12. import { storeToRefs } from 'pinia';
  13. import { useUserStore } from '@/store/modules/user';
  14. import loadingImg from '@/assets/images/nine-square-grid/loading.gif';
  15. const props = defineProps<{
  16. url: string;
  17. poster?: string;
  18. }>();
  19. const userStore = useUserStore();
  20. const { token } = storeToRefs(userStore);
  21. let player: mpegts.Player | null;
  22. let lastDecodedFrames = 0; // 10s前的解码帧数
  23. let currentDecodedFrames = 0; // 当前解码帧数
  24. let loadingTimeout = 0;
  25. const videoRef = ref<HTMLVideoElement | null>(null);
  26. const loading = ref(true);
  27. const urlWithToken = computed(() => {
  28. if (!props.url) return '';
  29. return props.url;
  30. });
  31. const handleLoadeddata = () => {
  32. loading.value = false;
  33. };
  34. const initPlay = () => {
  35. if (!props.url || !videoRef.value || !token.value) {
  36. return;
  37. }
  38. const videoElement = videoRef.value;
  39. lastDecodedFrames = 0;
  40. currentDecodedFrames = 0;
  41. player = mpegts.createPlayer(
  42. {
  43. type: 'flv',
  44. isLive: true,
  45. hasAudio: false,
  46. url: urlWithToken.value,
  47. },
  48. {
  49. liveBufferLatencyChasing: true,
  50. /**
  51. * 控制直播视频流在缓冲区中允许的最大延迟时间。
  52. * 较小的值使播放更接近实时,但可能会因为网络波动或数据传输不及时导致播放卡顿。
  53. * 较大的值则会允许更多的缓冲,这样可以更好地应对网络波动,减少卡顿的可能性,但会增加播放延迟。
  54. */
  55. liveBufferLatencyMaxLatency: 4,
  56. },
  57. );
  58. videoElement.removeEventListener('loadeddata', handleLoadeddata);
  59. videoElement.addEventListener('loadeddata', handleLoadeddata);
  60. player.attachMediaElement(videoElement);
  61. player.load();
  62. player.on(mpegts.Events.ERROR, (e, detail, data) => {
  63. loading.value = true;
  64. console.log('视频加载错误类型', e);
  65. console.log('视频加载错误详情类型', detail);
  66. console.log('视频加载错误信息', data);
  67. // 当发生error时,这里会发生死循环,所以要注销掉。 interval方式中已经包含了此种错误的处理
  68. // reloadPlayer();
  69. });
  70. player.on(mpegts.Events.RECOVERED_EARLY_EOF, () => {
  71. console.log('视频播放结束');
  72. loading.value = true;
  73. });
  74. player.on(mpegts.Events.STATISTICS_INFO, (e) => {
  75. // console.log("视频播放信息", e.decodedFrames);
  76. const frame = e.decodedFrames || 0;
  77. handleLoading(frame);
  78. currentDecodedFrames = frame;
  79. });
  80. // player.play();
  81. setTimeout(() => {
  82. player?.play() as Promise<void>;
  83. console.log('视频play()触发');
  84. }, 50);
  85. };
  86. /** 处理loading信息 */
  87. const handleLoading = (nextFrame: number) => {
  88. if (currentDecodedFrames === nextFrame) {
  89. if (loadingTimeout) return;
  90. loadingTimeout = window.setTimeout(() => {
  91. loading.value = true;
  92. loadingTimeout = 0;
  93. }, 6000);
  94. } else {
  95. clearTimeout(loadingTimeout);
  96. loadingTimeout = 0;
  97. loading.value = false;
  98. }
  99. };
  100. const interval = setInterval(() => {
  101. if (currentDecodedFrames === lastDecodedFrames) {
  102. console.log('视频播放卡顿,10s前解码帧数为 ' + lastDecodedFrames + ' ,当前解码帧数为 ' + currentDecodedFrames);
  103. reloadPlayer();
  104. }
  105. lastDecodedFrames = currentDecodedFrames;
  106. }, 15000);
  107. const destroyPlayer = () => {
  108. // liveLoaded.value = false;
  109. if (player) {
  110. console.log('视频判断需要销毁');
  111. player!.pause();
  112. player!.unload();
  113. player!.detachMediaElement();
  114. console.log('视频播放器销毁');
  115. player!.destroy();
  116. player = null;
  117. }
  118. };
  119. onMounted(() => {
  120. initPlay();
  121. });
  122. const reloadPlayer = () => {
  123. loading.value = true;
  124. destroyPlayer();
  125. setTimeout(() => {
  126. initPlay();
  127. }, 100);
  128. };
  129. //切换播放url
  130. watch(
  131. () => props.url,
  132. () => {
  133. if (props.url) {
  134. reloadPlayer();
  135. }
  136. },
  137. {
  138. deep: true,
  139. },
  140. );
  141. onBeforeUnmount(() => {
  142. destroyPlayer();
  143. clearInterval(interval);
  144. clearTimeout(loadingTimeout);
  145. });
  146. </script>
  147. <style scoped lang="less">
  148. .video-content {
  149. width: 100%;
  150. height: 100%;
  151. background-color: transparent !important;
  152. object-fit: contain; // 0729fill->contain: 应急处置-指挥中心按需修改,若有其余地方使用出错再行修改
  153. }
  154. .videoWrapper {
  155. width: 100%;
  156. height: 100%;
  157. display: inline-block;
  158. position: relative;
  159. }
  160. .loading {
  161. position: absolute;
  162. z-index: 1;
  163. top: 0;
  164. bottom: 0;
  165. left: 0;
  166. right: 0;
  167. margin: auto;
  168. width: 60px;
  169. }
  170. </style>