Bläddra i källkod

初始化提交

sangkf 7 månader sedan
förälder
incheckning
9beb2aa2cf
4 ändrade filer med 898 tillägg och 867 borttagningar
  1. 238 860
      package-lock.json
  2. 5 0
      package.json
  3. 69 7
      src/App.vue
  4. 586 0
      src/components/VideoPlayer.vue

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 238 - 860
package-lock.json


+ 5 - 0
package.json

@@ -9,6 +9,11 @@
   },
   "dependencies": {
     "core-js": "^3.8.3",
+    "flv.js": "^1.6.2",
+    "hls.js": "^1.5.20",
+    "video.js": "^7.21.6",
+    "videojs-contrib-hls": "^5.15.0",
+    "videojs-contrib-quality-levels": "^2.2.1",
     "vue": "^2.6.14"
   },
   "devDependencies": {

+ 69 - 7
src/App.vue

@@ -1,28 +1,90 @@
 <template>
   <div id="app">
-    <img alt="Vue logo" src="./assets/logo.png">
-    <HelloWorld msg="Welcome to Your Vue.js App"/>
+    <header class="app-header">
+      <h1>FLV 视频播放器</h1>
+    </header>
+    <main>
+      <VideoPlayer />
+    </main>
+    <footer class="app-footer">
+      <p>基于 Vue.js 和 flv.js 构建的 Material Design 风格视频播放器</p>
+    </footer>
   </div>
 </template>
 
 <script>
-import HelloWorld from './components/HelloWorld.vue'
+import VideoPlayer from './components/VideoPlayer.vue'
 
 export default {
   name: 'App',
   components: {
-    HelloWorld
+    VideoPlayer
   }
 }
 </script>
 
 <style>
+body {
+  margin: 0;
+  padding: 0;
+  background-color: #f5f5f7;
+  color: #1d1d1f;
+}
+
 #app {
-  font-family: Avenir, Helvetica, Arial, sans-serif;
+  font-family: 'Roboto', sans-serif;
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
   text-align: center;
-  color: #2c3e50;
-  margin-top: 60px;
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 20px;
+}
+
+.app-header {
+  margin-bottom: 20px;
+}
+
+.logo {
+  height: 80px;
+  margin-bottom: 10px;
+}
+
+h1 {
+  font-size: 32px;
+  font-weight: 600;
+  margin: 10px 0;
+  color: #1d1d1f;
+}
+
+.tips-container {
+  margin-bottom: 20px;
+  padding: 12px 16px;
+  background-color: #e3f2fd;
+  border-radius: 8px;
+  text-align: left;
+}
+
+.tip-item {
+  display: flex;
+  align-items: center;
+}
+
+.tip-icon {
+  margin-right: 10px;
+  font-style: normal;
+}
+
+main {
+  background-color: white;
+  border-radius: 8px;
+  padding: 30px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
+}
+
+.app-footer {
+  margin-top: 30px;
+  font-size: 14px;
+  color: #86868b;
 }
 </style>

+ 586 - 0
src/components/VideoPlayer.vue

@@ -0,0 +1,586 @@
+<template>
+  <div class="video-container">
+    <div class="video-wrapper">
+      <video
+        ref="videoElement"
+        class="video-player video-js vjs-default-skin"
+        controls
+        autoplay
+      ></video>
+    </div>
+    <div class="control-panel">
+      <div class="format-decoder-options">
+        <div class="format-selector">
+          <label class="format-label">
+            <input type="radio" v-model="videoFormat" value="flv">
+            <span>FLV</span>
+          </label>
+          <label class="format-label">
+            <input type="radio" v-model="videoFormat" value="hls">
+            <span>HLS (H.265)</span>
+          </label>
+          <label class="format-label">
+            <input type="radio" v-model="videoFormat" value="mp4">
+            <span>MP4</span>
+          </label>
+        </div>
+        <div class="decoder-options">
+          <label class="decoder-label">
+            <input type="radio" v-model="decoderType" value="hardware">
+            <span>硬件加速</span>
+          </label>
+          <label class="decoder-label">
+            <input type="radio" v-model="decoderType" value="software">
+            <span>软件解码</span>
+          </label>
+          <label class="decoder-label">
+            <input type="radio" v-model="decoderType" value="auto">
+            <span>自动选择</span>
+          </label>
+          <label class="decoder-label">
+            <input type="radio" v-model="decoderType" value="none">
+            <span>关闭解码</span>
+          </label>
+        </div>
+      </div>
+      <div class="video-controls">
+        <input 
+          type="text" 
+          v-model="videoUrl" 
+          placeholder="请输入视频URL" 
+          class="video-url-input"
+        />
+        <button @click="loadVideo" class="load-button">
+          <span class="button-text">播放</span>
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import flvjs from 'flv.js'
+import Hls from 'hls.js'
+import 'video.js/dist/video-js.css'
+import videojs from 'video.js'
+import 'videojs-contrib-hls'
+import 'videojs-contrib-quality-levels'
+
+export default {
+  name: 'VideoPlayer',
+  data() {
+    return {
+      videoUrl: '',
+      flvPlayer: null,
+      hlsPlayer: null,
+      vjsPlayer: null,
+      videoFormat: 'flv',
+      decoderType: 'auto',
+      showPlayButton: false
+    }
+  },
+  methods: {
+    loadVideo() {
+      if (!this.videoUrl) {
+        alert('请输入视频URL');
+        return;
+      }
+      
+      // 销毁之前的播放器实例
+      this.destroyPlayer();
+      
+      const videoElement = this.$refs.videoElement;
+      
+      // 根据选择的格式和解码选项加载视频
+      if (this.decoderType === 'hardware') {
+        // 使用 video.js 进行硬件加速解码
+        this.loadWithVideoJS();
+      } else if (this.decoderType === 'software') {
+        // 使用软件解码
+        this.loadWithSoftwareDecoder();
+      } else if (this.decoderType === 'none') {
+        // 关闭解码,直接使用浏览器原生播放
+        this.loadWithoutDecoder();
+      } else {
+        // 自动选择解码方式
+        if (this.videoFormat === 'flv') {
+          this.loadFlvVideo(videoElement);
+        } else if (this.videoFormat === 'hls') {
+          this.loadHlsVideo(videoElement);
+        } else if (this.videoFormat === 'mp4') {
+          this.loadMp4Video(videoElement);
+        }
+      }
+    },
+    
+    loadWithoutDecoder() {
+      const videoElement = this.$refs.videoElement;
+      
+      // 移除 video.js 相关类
+      videoElement.classList.remove('video-js', 'vjs-default-skin');
+      
+      // 根据不同格式尝试直接播放
+      if (this.videoFormat === 'mp4') {
+        // MP4 格式大多数浏览器原生支持
+        videoElement.src = this.videoUrl;
+        videoElement.load();
+        videoElement.play().catch(e => {
+          console.error('原生播放失败:', e);
+          alert('您的浏览器可能不支持直接播放此格式,请尝试启用解码器');
+        });
+      } else if (this.videoFormat === 'hls' && videoElement.canPlayType('application/vnd.apple.mpegurl')) {
+        // 某些浏览器(Safari)原生支持HLS
+        videoElement.src = this.videoUrl;
+        videoElement.addEventListener('loadedmetadata', () => {
+          videoElement.play().catch(e => {
+            console.error('原生HLS播放失败:', e);
+          });
+        });
+      } else {
+        // 其他格式尝试直接播放,但可能不支持
+        videoElement.src = this.videoUrl;
+        videoElement.load();
+        videoElement.play().catch(e => {
+          console.error('原生播放失败:', e);
+          alert(`您的浏览器不支持直接播放${this.videoFormat.toUpperCase()}格式,请启用解码器`);
+        });
+      }
+    },
+    
+    loadWithSoftwareDecoder() {
+      const videoElement = this.$refs.videoElement;
+      
+      if (this.videoFormat === 'flv') {
+        // FLV 软件解码
+        if (flvjs.isSupported()) {
+          this.flvPlayer = flvjs.createPlayer({
+            type: 'flv',
+            url: this.videoUrl,
+            hasAudio: true,
+            hasVideo: true,
+            isLive: true,
+            cors: true,
+            withCredentials: false,
+            enableStashBuffer: true,
+            stashInitialSize: 1024 * 256, // 更大的缓冲区
+            enableWorker: true, // 启用 Web Worker
+            softwareDecoder: true, // 启用软件解码
+            softwareDecodeErrorRecover: true, // 软件解码错误恢复
+            lazyLoad: false,
+            seekType: 'range',
+            rangeLoadZeroStart: true,
+            fixAudioTimestampGap: true,
+          });
+          
+          this.flvPlayer.attachMediaElement(videoElement);
+          this.flvPlayer.load();
+          
+          // 添加错误处理
+          this.flvPlayer.on(flvjs.Events.ERROR, (errorType, errorDetail) => {
+            console.error('FLV软件解码错误:', errorType, errorDetail);
+            
+            // 尝试恢复播放
+            if (errorType === flvjs.ErrorTypes.NETWORK_ERROR) {
+              setTimeout(() => {
+                console.log('尝试重新加载视频...');
+                this.flvPlayer.unload();
+                this.flvPlayer.load();
+                this.flvPlayer.play();
+              }, 3000);
+            } else if (errorType === flvjs.ErrorTypes.MEDIA_ERROR) {
+              // 媒体错误,尝试重新创建播放器
+              setTimeout(() => {
+                console.log('尝试重新创建播放器...');
+                this.destroyPlayer();
+                this.loadWithSoftwareDecoder();
+              }, 2000);
+            }
+          });
+          
+          this.flvPlayer.play().catch(e => {
+            console.error('软件解码播放失败:', e);
+          });
+        } else {
+          alert('您的浏览器不支持FLV视频播放');
+        }
+      } else if (this.videoFormat === 'hls') {
+        // HLS 软件解码
+        if (Hls.isSupported()) {
+          this.hlsPlayer = new Hls({
+            enableWorker: true,
+            lowLatencyMode: false, // 关闭低延迟模式以提高稳定性
+            maxBufferLength: 60,
+            maxMaxBufferLength: 120,
+            backBufferLength: 90,
+            enableSoftwareAES: true, // 启用软件 AES 解密
+            fragLoadingTimeOut: 20000, // 增加片段加载超时
+            manifestLoadingTimeOut: 20000, // 增加清单加载超时
+            levelLoadingTimeOut: 20000, // 增加级别加载超时
+            startFragPrefetch: true, // 预取片段
+          });
+          
+          this.hlsPlayer.loadSource(this.videoUrl);
+          this.hlsPlayer.attachMedia(videoElement);
+          
+          this.hlsPlayer.on(Hls.Events.MANIFEST_PARSED, () => {
+            videoElement.play();
+          });
+          
+          this.hlsPlayer.on(Hls.Events.ERROR, (event, data) => {
+            console.error('HLS软件解码错误:', data);
+            if (data.fatal) {
+              switch(data.type) {
+                case Hls.ErrorTypes.NETWORK_ERROR:
+                  console.error('网络错误,尝试恢复...');
+                  this.hlsPlayer.startLoad();
+                  break;
+                case Hls.ErrorTypes.MEDIA_ERROR:
+                  console.error('媒体错误,尝试恢复...');
+                  this.hlsPlayer.recoverMediaError();
+                  break;
+                default:
+                  console.error('无法恢复的错误,重新创建播放器...');
+                  setTimeout(() => {
+                    this.destroyPlayer();
+                    this.loadWithSoftwareDecoder();
+                  }, 2000);
+                  break;
+              }
+            }
+          });
+        } else {
+          alert('您的浏览器不支持HLS视频播放');
+        }
+      } else {
+        // MP4 软件解码 (使用原生播放器)
+        this.loadMp4Video(videoElement);
+      }
+    },
+    
+    loadWithVideoJS() {
+      // 初始化 video.js 播放器
+      this.vjsPlayer = videojs(this.$refs.videoElement, {
+        techOrder: ['html5'],
+        sources: [{
+          src: this.videoUrl,
+          type: this.getVideoJSType()
+        }],
+        controls: true,
+        autoplay: true,
+        preload: 'auto',
+        fluid: true,
+        html5: {
+          hls: {
+            overrideNative: true,
+            enableLowInitialPlaylist: true,
+            smoothQualityChange: true,
+            handleManifestRedirects: true
+          },
+          nativeVideoTracks: false,
+          nativeAudioTracks: false,
+          nativeTextTracks: false
+        }
+      });
+      
+      // 添加质量选择功能
+      this.vjsPlayer.qualityLevels();
+      
+      // 错误处理
+      this.vjsPlayer.on('error', () => {
+        console.error('VideoJS 播放错误:', this.vjsPlayer.error());
+      });
+    },
+    
+    getVideoJSType() {
+      switch(this.videoFormat) {
+        case 'flv':
+          return 'video/x-flv';
+        case 'hls':
+          return 'application/x-mpegURL';
+        case 'mp4':
+          return 'video/mp4';
+        default:
+          return '';
+      }
+    },
+    
+    loadFlvVideo(videoElement) {
+      if (flvjs.isSupported()) {
+        this.flvPlayer = flvjs.createPlayer({
+          type: 'flv',
+          url: this.videoUrl,
+          hasAudio: true,
+          hasVideo: true,
+          isLive: true,
+          cors: true,
+          withCredentials: false,
+          enableStashBuffer: true,
+          stashInitialSize: 1024 * 128, // 增加缓冲区大小
+          lazyLoad: false, // 禁用懒加载
+          lazyLoadMaxDuration: 0,
+          seekType: 'range',
+          rangeLoadZeroStart: true, // 从0开始加载
+          fixAudioTimestampGap: true, // 修复音频时间戳间隙
+        });
+        
+        this.flvPlayer.attachMediaElement(videoElement);
+        this.flvPlayer.load();
+        
+        // 添加错误处理
+        this.flvPlayer.on(flvjs.Events.ERROR, (errorType, errorDetail) => {
+          console.error('FLV播放错误:', errorType, errorDetail);
+          
+          // 尝试恢复播放
+          if (errorType === flvjs.ErrorTypes.NETWORK_ERROR) {
+            // 网络错误,尝试重新加载
+            setTimeout(() => {
+              console.log('尝试重新加载视频...');
+              this.flvPlayer.unload();
+              this.flvPlayer.load();
+              this.flvPlayer.play();
+            }, 3000);
+          }
+        });
+        
+        this.flvPlayer.play().catch(e => {
+          console.error('播放失败:', e);
+          // 可能是自动播放策略阻止,添加手动播放按钮
+          this.showPlayButton = true;
+        });
+      } else {
+        alert('您的浏览器不支持FLV视频播放');
+      }
+    },
+    
+    loadHlsVideo(videoElement) {
+      if (videoElement.canPlayType('application/vnd.apple.mpegurl')) {
+        videoElement.src = this.videoUrl;
+        videoElement.addEventListener('loadedmetadata', () => {
+          videoElement.play();
+        });
+      } else if (Hls.isSupported()) {
+        this.hlsPlayer = new Hls({
+          enableWorker: true,
+          lowLatencyMode: true,
+          maxBufferLength: 30,
+          maxMaxBufferLength: 60
+        });
+        this.hlsPlayer.loadSource(this.videoUrl);
+        this.hlsPlayer.attachMedia(videoElement);
+        this.hlsPlayer.on(Hls.Events.MANIFEST_PARSED, () => {
+          videoElement.play();
+        });
+        
+        this.hlsPlayer.on(Hls.Events.ERROR, (event, data) => {
+          if (data.fatal) {
+            switch(data.type) {
+              case Hls.ErrorTypes.NETWORK_ERROR:
+                console.error('网络错误');
+                this.hlsPlayer.startLoad();
+                break;
+              case Hls.ErrorTypes.MEDIA_ERROR:
+                console.error('媒体错误');
+                this.hlsPlayer.recoverMediaError();
+                break;
+              default:
+                console.error('无法恢复的错误', data);
+                this.destroyPlayer();
+                break;
+            }
+          }
+        });
+      } else {
+        alert('您的浏览器不支持HLS视频播放');
+      }
+    },
+    
+    loadMp4Video(videoElement) {
+      videoElement.src = this.videoUrl;
+      videoElement.load();
+      videoElement.play();
+    },
+    
+    destroyPlayer() {
+      if (this.flvPlayer) {
+        this.flvPlayer.unload();
+        this.flvPlayer.detachMediaElement();
+        this.flvPlayer.destroy();
+        this.flvPlayer = null;
+      }
+      
+      if (this.hlsPlayer) {
+        this.hlsPlayer.destroy();
+        this.hlsPlayer = null;
+      }
+      
+      if (this.vjsPlayer) {
+        this.vjsPlayer.dispose();
+        this.vjsPlayer = null;
+      }
+      
+      const videoElement = this.$refs.videoElement;
+      if (videoElement) {
+        videoElement.src = '';
+        videoElement.removeAttribute('src');
+      }
+    }
+  },
+  beforeDestroy() {
+    this.destroyPlayer();
+  }
+}
+</script>
+
+<style scoped>
+.video-container {
+  width: 100%;
+  max-width: 1200px;
+  margin: 0 auto;
+  font-family: 'Roboto', sans-serif;
+  display: flex;
+  flex-direction: column;
+}
+
+.video-wrapper {
+  width: 100%;
+  position: relative;
+  background-color: #000;
+  border-radius: 4px 4px 0 0;
+  overflow: hidden;
+}
+
+.video-player {
+  width: 100%;
+  height: 100%;
+  min-height: 500px;
+  display: block;
+  background-color: #000;
+}
+
+.control-panel {
+  width: 100%;
+  padding: 16px;
+  background-color: #fff;
+  border-radius: 0 0 4px 4px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.format-decoder-options {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+  margin-bottom: 16px;
+}
+
+.format-selector {
+  display: flex;
+  gap: 16px;
+  margin-bottom: 8px;
+}
+
+.format-label {
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  font-size: 14px;
+  color: #424242;
+}
+
+.format-label input {
+  margin-right: 6px;
+}
+
+.decoder-options {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+
+.decoder-label {
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  font-size: 14px;
+  color: #424242;
+  margin-right: 10px;
+}
+
+.decoder-label input {
+  margin-right: 6px;
+}
+
+.video-controls {
+  display: flex;
+  gap: 12px;
+  width: 100%;
+}
+
+.video-url-input {
+  flex: 1;
+  padding: 12px;
+  border-radius: 4px;
+  border: 1px solid #e0e0e0;
+  font-size: 14px;
+  background-color: #fff;
+  transition: all 0.2s ease;
+  outline: none;
+  box-sizing: border-box;
+}
+
+.video-url-input:focus {
+  border-color: #6200ee;
+  box-shadow: 0 0 0 2px rgba(98, 0, 238, 0.2);
+}
+
+.load-button {
+  padding: 10px 24px;
+  background-color: #6200ee;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+  white-space: nowrap;
+}
+
+.button-text {
+  margin-left: 4px;
+}
+
+.load-button:hover {
+  background-color: #7c4dff;
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+}
+
+.load-button:active {
+  background-color: #5600e8;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
+}
+
+@media (max-width: 768px) {
+  .format-decoder-options {
+    flex-direction: column;
+  }
+  
+  .format-selector, .decoder-options {
+    flex-wrap: wrap;
+    margin-bottom: 12px;
+  }
+  
+  .video-controls {
+    flex-direction: column;
+  }
+  
+  .load-button {
+    width: 100%;
+  }
+}
+</style>

Vissa filer visades inte eftersom för många filer har ändrats