|
|
@@ -1,12 +1,17 @@
|
|
|
<template>
|
|
|
<div class="video-container">
|
|
|
<div class="video-wrapper">
|
|
|
+ <!-- 添加 Jessibuca 播放器容器 -->
|
|
|
+ <div v-if="playerType === 'jessibuca'" ref="jessibucaContainer" class="jessibuca-container"></div>
|
|
|
+
|
|
|
<video
|
|
|
+ v-else
|
|
|
ref="videoElement"
|
|
|
class="video-player video-js vjs-default-skin"
|
|
|
controls
|
|
|
autoplay
|
|
|
></video>
|
|
|
+
|
|
|
<!-- 添加播放状态指示器 -->
|
|
|
<div v-if="playStatus !== 'playing' && playStatus !== 'paused'" class="play-status-overlay">
|
|
|
<div class="status-indicator">
|
|
|
@@ -19,48 +24,80 @@
|
|
|
</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="m3u8">
|
|
|
- <span>M3U8</span>
|
|
|
- </label>
|
|
|
- <label class="format-label">
|
|
|
- <input type="radio" v-model="videoFormat" value="mp4">
|
|
|
- <span>MP4</span>
|
|
|
- </label>
|
|
|
+ <!-- 播放器选择 -->
|
|
|
+ <div class="option-group">
|
|
|
+ <div class="option-title">播放器选择:</div>
|
|
|
+ <div class="player-selector">
|
|
|
+ <label class="player-label">
|
|
|
+ <input type="radio" v-model="playerType" value="jessibuca" checked>
|
|
|
+ <span>Jessibuca</span>
|
|
|
+ </label>
|
|
|
+ <label class="player-label">
|
|
|
+ <input type="radio" v-model="playerType" value="videojs">
|
|
|
+ <span>VideoJS</span>
|
|
|
+ </label>
|
|
|
+ <label class="player-label">
|
|
|
+ <input type="radio" v-model="playerType" value="native">
|
|
|
+ <span>原生播放器</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
</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 class="option-group">
|
|
|
+ <div class="option-title">视频格式:</div>
|
|
|
+ <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="m3u8">
|
|
|
+ <span>M3U8</span>
|
|
|
+ </label>
|
|
|
+ <label class="format-label">
|
|
|
+ <input type="radio" v-model="videoFormat" value="mp4">
|
|
|
+ <span>MP4</span>
|
|
|
+ </label>
|
|
|
+ <label class="format-label">
|
|
|
+ <input type="radio" v-model="videoFormat" value="ws">
|
|
|
+ <span>WebSocket</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 解码方式选择 -->
|
|
|
+ <div class="option-group" v-if="playerType !== 'jessibuca'">
|
|
|
+ <div class="option-title">解码方式:</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>
|
|
|
<div class="video-controls">
|
|
|
- <input
|
|
|
- type="text"
|
|
|
- v-model="videoUrl"
|
|
|
- placeholder="请输入视频URL"
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ v-model="videoUrl"
|
|
|
+ placeholder="请输入视频URL"
|
|
|
class="video-url-input"
|
|
|
/>
|
|
|
<button @click="loadVideo" class="load-button">
|
|
|
@@ -78,6 +115,10 @@ import 'video.js/dist/video-js.css'
|
|
|
import videojs from 'video.js'
|
|
|
import 'videojs-contrib-hls'
|
|
|
import 'videojs-contrib-quality-levels'
|
|
|
+// 导入 Jessibuca
|
|
|
+import Jessibuca from '@/assets/jessibuca/jessibuca.js'
|
|
|
+// 导入 Jessibuca 配置
|
|
|
+import { getJessibucaConfig, getJessibucaUrl } from '@/utils/jessibucaConfig'
|
|
|
|
|
|
export default {
|
|
|
name: 'VideoPlayer',
|
|
|
@@ -87,11 +128,16 @@ export default {
|
|
|
flvPlayer: null,
|
|
|
hlsPlayer: null,
|
|
|
vjsPlayer: null,
|
|
|
+ wsConnection: null,
|
|
|
+ mediaSource: null,
|
|
|
+ sourceBuffer: null,
|
|
|
+ // 添加 Jessibuca 相关数据
|
|
|
+ jessibucaPlayer: null,
|
|
|
videoFormat: 'flv',
|
|
|
- decoderType: 'auto',
|
|
|
+ playerType: 'jessibuca', // 播放器类型
|
|
|
+ decoderType: 'auto', // 解码方式
|
|
|
showPlayButton: false,
|
|
|
- // 添加播放状态相关数据
|
|
|
- playStatus: 'idle', // idle, loading, buffering, playing, paused, error
|
|
|
+ playStatus: 'idle',
|
|
|
errorMessage: ''
|
|
|
}
|
|
|
},
|
|
|
@@ -115,41 +161,50 @@ export default {
|
|
|
alert('请输入视频URL');
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 设置加载状态
|
|
|
this.playStatus = 'loading';
|
|
|
this.errorMessage = '';
|
|
|
-
|
|
|
+
|
|
|
// 销毁之前的播放器实例
|
|
|
this.destroyPlayer();
|
|
|
-
|
|
|
- const videoElement = this.$refs.videoElement;
|
|
|
-
|
|
|
- // 添加通用事件监听
|
|
|
- this.addVideoEventListeners(videoElement);
|
|
|
-
|
|
|
- // 根据选择的格式和解码选项加载视频
|
|
|
- if (this.decoderType === 'hardware') {
|
|
|
- // 使用 video.js 进行硬件加速解码
|
|
|
+
|
|
|
+ // 根据选择的播放器类型加载视频
|
|
|
+ if (this.playerType === 'jessibuca') {
|
|
|
+ // 使用 Jessibuca 播放器
|
|
|
+ this.loadWithJessibuca();
|
|
|
+ } else if (this.playerType === 'videojs') {
|
|
|
+ // 使用 VideoJS 播放器
|
|
|
+ const videoElement = this.$refs.videoElement;
|
|
|
+ this.addVideoEventListeners(videoElement);
|
|
|
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.videoFormat === 'm3u8') {
|
|
|
- this.loadHlsVideo(videoElement);
|
|
|
- } else if (this.videoFormat === 'mp4') {
|
|
|
- this.loadMp4Video(videoElement);
|
|
|
+ } else if (this.playerType === 'native') {
|
|
|
+ // 使用原生播放器
|
|
|
+ const videoElement = this.$refs.videoElement;
|
|
|
+ this.addVideoEventListeners(videoElement);
|
|
|
+
|
|
|
+ // 根据解码方式和视频格式选择加载方法
|
|
|
+ if (this.decoderType === 'hardware') {
|
|
|
+ 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.videoFormat === 'm3u8') {
|
|
|
+ this.loadHlsVideo(videoElement);
|
|
|
+ } else if (this.videoFormat === 'mp4') {
|
|
|
+ this.loadMp4Video(videoElement);
|
|
|
+ } else if (this.videoFormat === 'ws') {
|
|
|
+ this.loadWebSocketVideo(videoElement);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
-
|
|
|
+
|
|
|
// 添加视频事件监听器
|
|
|
addVideoEventListeners(videoElement) {
|
|
|
// 移除之前的事件监听器
|
|
|
@@ -165,7 +220,7 @@ export default {
|
|
|
videoElement.addEventListener('error', this.handleError);
|
|
|
},
|
|
|
|
|
|
- // 事件处理函数
|
|
|
+ // 视频事件处理函数
|
|
|
handlePlaying() {
|
|
|
this.playStatus = 'playing';
|
|
|
},
|
|
|
@@ -179,212 +234,42 @@ export default {
|
|
|
},
|
|
|
|
|
|
handleError(e) {
|
|
|
- this.playStatus = 'error';
|
|
|
- this.errorMessage = e.target.error ? e.target.error.message : '未知错误';
|
|
|
console.error('视频播放错误:', e);
|
|
|
+ this.playStatus = 'error';
|
|
|
+ this.errorMessage = `视频播放错误: ${e.target.error ? e.target.error.message : '未知错误'}`;
|
|
|
},
|
|
|
-
|
|
|
- loadWithoutDecoder() {
|
|
|
+
|
|
|
+ // 使用 VideoJS 播放视频
|
|
|
+ loadWithVideoJS() {
|
|
|
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);
|
|
|
- this.playStatus = 'error';
|
|
|
- this.errorMessage = '浏览器不支持直接播放此格式';
|
|
|
- alert('您的浏览器可能不支持直接播放此格式,请尝试启用解码器');
|
|
|
- });
|
|
|
- }
|
|
|
- else if ((this.videoFormat === 'hls' || this.videoFormat === 'm3u8') &&
|
|
|
- videoElement.canPlayType('application/vnd.apple.mpegurl')) {
|
|
|
- // 某些浏览器(Safari)原生支持HLS/M3U8
|
|
|
- videoElement.src = this.videoUrl;
|
|
|
- videoElement.addEventListener('loadedmetadata', () => {
|
|
|
- videoElement.play().catch(e => {
|
|
|
- console.error('原生HLS/M3U8播放失败:', e);
|
|
|
- this.playStatus = 'error';
|
|
|
- this.errorMessage = 'HLS/M3U8播放失败';
|
|
|
- });
|
|
|
- });
|
|
|
- } else {
|
|
|
- // 其他格式尝试直接播放,但可能不支持
|
|
|
- videoElement.src = this.videoUrl;
|
|
|
- videoElement.load();
|
|
|
- videoElement.play().catch(e => {
|
|
|
- console.error('原生播放失败:', e);
|
|
|
- alert(`您的浏览器不支持直接播放${this.videoFormat.toUpperCase()}格式,请启用解码器`);
|
|
|
- });
|
|
|
+ if (this.vjsPlayer) {
|
|
|
+ this.vjsPlayer.dispose();
|
|
|
}
|
|
|
- },
|
|
|
-
|
|
|
- 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);
|
|
|
- this.playStatus = 'error';
|
|
|
- this.errorMessage = `FLV解码错误: ${errorType}`;
|
|
|
-
|
|
|
- // 尝试恢复播放
|
|
|
- if (errorType === flvjs.ErrorTypes.NETWORK_ERROR) {
|
|
|
- setTimeout(() => {
|
|
|
- console.log('尝试重新加载视频...');
|
|
|
- this.playStatus = 'loading';
|
|
|
- this.flvPlayer.unload();
|
|
|
- this.flvPlayer.load();
|
|
|
- this.flvPlayer.play();
|
|
|
- }, 3000);
|
|
|
- } else if (errorType === flvjs.ErrorTypes.MEDIA_ERROR) {
|
|
|
- // 媒体错误,尝试重新创建播放器
|
|
|
- setTimeout(() => {
|
|
|
- console.log('尝试重新创建播放器...');
|
|
|
- this.playStatus = 'loading';
|
|
|
- this.destroyPlayer();
|
|
|
- this.loadWithSoftwareDecoder();
|
|
|
- }, 2000);
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- this.flvPlayer.play().catch(e => {
|
|
|
- console.error('软件解码播放失败:', e);
|
|
|
- this.playStatus = 'error';
|
|
|
- this.errorMessage = '软件解码播放失败';
|
|
|
- });
|
|
|
- } else {
|
|
|
- this.playStatus = 'error';
|
|
|
- this.errorMessage = '浏览器不支持FLV视频播放';
|
|
|
- alert('您的浏览器不支持FLV视频播放');
|
|
|
- }
|
|
|
- }
|
|
|
- else if (this.videoFormat === 'hls' || this.videoFormat === 'm3u8') {
|
|
|
- // HLS/M3U8 软件解码
|
|
|
- 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()
|
|
|
- }],
|
|
|
+ this.vjsPlayer = videojs(videoElement, {
|
|
|
controls: true,
|
|
|
autoplay: true,
|
|
|
preload: 'auto',
|
|
|
fluid: true,
|
|
|
- html5: {
|
|
|
- hls: {
|
|
|
- overrideNative: true,
|
|
|
- enableLowInitialPlaylist: true,
|
|
|
- smoothQualityChange: true,
|
|
|
- handleManifestRedirects: true
|
|
|
- },
|
|
|
- nativeVideoTracks: false,
|
|
|
- nativeAudioTracks: false,
|
|
|
- nativeTextTracks: false
|
|
|
- }
|
|
|
+ sources: [{
|
|
|
+ src: this.videoUrl,
|
|
|
+ type: this.getVideoJSType()
|
|
|
+ }]
|
|
|
});
|
|
|
|
|
|
- // 添加质量选择功能
|
|
|
- this.vjsPlayer.qualityLevels();
|
|
|
+ this.vjsPlayer.on('ready', () => {
|
|
|
+ console.log('VideoJS 准备就绪');
|
|
|
+ });
|
|
|
|
|
|
- // 错误处理
|
|
|
- this.vjsPlayer.on('error', () => {
|
|
|
- const error = this.vjsPlayer.error();
|
|
|
- console.error('VideoJS 播放错误:', error);
|
|
|
+ this.vjsPlayer.on('error', (e) => {
|
|
|
+ console.error('VideoJS 错误:', e);
|
|
|
this.playStatus = 'error';
|
|
|
- this.errorMessage = `VideoJS错误: ${error ? error.message : '未知错误'}`;
|
|
|
+ this.errorMessage = `VideoJS 错误: ${this.vjsPlayer.error().message}`;
|
|
|
});
|
|
|
-
|
|
|
- // 添加播放状态监听
|
|
|
- this.vjsPlayer.on('playing', this.handlePlaying);
|
|
|
- this.vjsPlayer.on('pause', this.handlePause);
|
|
|
- this.vjsPlayer.on('waiting', this.handleWaiting);
|
|
|
},
|
|
|
|
|
|
+ // 获取 VideoJS 的视频类型
|
|
|
getVideoJSType() {
|
|
|
switch(this.videoFormat) {
|
|
|
case 'flv':
|
|
|
@@ -394,143 +279,359 @@ export default {
|
|
|
return 'application/x-mpegURL';
|
|
|
case 'mp4':
|
|
|
return 'video/mp4';
|
|
|
+ case 'ws':
|
|
|
+ return 'video/mp4'; // 假设 WebSocket 流是 MP4 格式
|
|
|
default:
|
|
|
return '';
|
|
|
}
|
|
|
},
|
|
|
|
|
|
+ // 使用软件解码播放视频
|
|
|
+ loadWithSoftwareDecoder() {
|
|
|
+ if (this.videoFormat === 'flv') {
|
|
|
+ this.loadFlvVideo(this.$refs.videoElement);
|
|
|
+ } else if (this.videoFormat === 'hls' || this.videoFormat === 'm3u8') {
|
|
|
+ this.loadHlsVideo(this.$refs.videoElement);
|
|
|
+ } else if (this.videoFormat === 'mp4') {
|
|
|
+ this.loadMp4Video(this.$refs.videoElement);
|
|
|
+ } else if (this.videoFormat === 'ws') {
|
|
|
+ this.loadWebSocketVideo(this.$refs.videoElement);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 不使用解码器直接播放视频
|
|
|
+ loadWithoutDecoder() {
|
|
|
+ const videoElement = this.$refs.videoElement;
|
|
|
+
|
|
|
+ if (this.videoFormat === 'flv' && !flvjs.isSupported()) {
|
|
|
+ this.playStatus = 'error';
|
|
|
+ this.errorMessage = '浏览器不支持直接播放FLV格式';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ((this.videoFormat === 'hls' || this.videoFormat === 'm3u8') &&
|
|
|
+ !videoElement.canPlayType('application/vnd.apple.mpegurl')) {
|
|
|
+ this.playStatus = 'error';
|
|
|
+ this.errorMessage = '浏览器不支持直接播放HLS/M3U8格式';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ videoElement.src = this.videoUrl;
|
|
|
+ videoElement.load();
|
|
|
+ videoElement.play();
|
|
|
+ },
|
|
|
+
|
|
|
+ // 使用 Jessibuca 播放视频
|
|
|
+ loadWithJessibuca() {
|
|
|
+ // 获取 Jessibuca 容器
|
|
|
+ const container = this.$refs.jessibucaContainer;
|
|
|
+
|
|
|
+ // 销毁之前的实例
|
|
|
+ if (this.jessibucaPlayer) {
|
|
|
+ this.jessibucaPlayer.destroy();
|
|
|
+ this.jessibucaPlayer = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取 Jessibuca 配置
|
|
|
+ const config = getJessibucaConfig(container);
|
|
|
+
|
|
|
+ // 创建 Jessibuca 实例
|
|
|
+ this.jessibucaPlayer = new Jessibuca(config);
|
|
|
+
|
|
|
+ // 处理 Jessibuca 事件
|
|
|
+ this.jessibucaPlayer.on('load', () => {
|
|
|
+ console.log('Jessibuca 加载成功');
|
|
|
+ });
|
|
|
+
|
|
|
+ this.jessibucaPlayer.on('play', () => {
|
|
|
+ console.log('Jessibuca 开始播放');
|
|
|
+ this.playStatus = 'playing';
|
|
|
+ });
|
|
|
+
|
|
|
+ this.jessibucaPlayer.on('pause', () => {
|
|
|
+ console.log('Jessibuca 暂停播放');
|
|
|
+ this.playStatus = 'paused';
|
|
|
+ });
|
|
|
+
|
|
|
+ this.jessibucaPlayer.on('error', (e) => {
|
|
|
+ console.error('Jessibuca 错误:', e);
|
|
|
+ this.playStatus = 'error';
|
|
|
+ this.errorMessage = `Jessibuca 错误: ${e}`;
|
|
|
+ });
|
|
|
+
|
|
|
+ this.jessibucaPlayer.on('timeout', () => {
|
|
|
+ console.error('Jessibuca 加载超时');
|
|
|
+ this.playStatus = 'error';
|
|
|
+ this.errorMessage = 'Jessibuca 加载超时';
|
|
|
+ });
|
|
|
+
|
|
|
+ this.jessibucaPlayer.on('fullscreen', (isFullscreen) => {
|
|
|
+ console.log('Jessibuca 全屏状态:', isFullscreen);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 获取处理后的 URL
|
|
|
+ const url = getJessibucaUrl(this.videoUrl, this.videoFormat);
|
|
|
+
|
|
|
+ // 开始播放
|
|
|
+ this.jessibucaPlayer.play(url);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 使用 FLV.js 播放 FLV 视频
|
|
|
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视频播放');
|
|
|
+ if (!flvjs.isSupported()) {
|
|
|
+ this.playStatus = 'error';
|
|
|
+ this.errorMessage = '浏览器不支持 FLV 播放';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 销毁之前的实例
|
|
|
+ if (this.flvPlayer) {
|
|
|
+ this.flvPlayer.destroy();
|
|
|
+ this.flvPlayer = null;
|
|
|
}
|
|
|
+
|
|
|
+ // 创建 FLV 播放器
|
|
|
+ this.flvPlayer = flvjs.createPlayer({
|
|
|
+ type: 'flv',
|
|
|
+ url: this.videoUrl,
|
|
|
+ isLive: true,
|
|
|
+ hasAudio: true,
|
|
|
+ hasVideo: true,
|
|
|
+ cors: true,
|
|
|
+ withCredentials: false
|
|
|
+ });
|
|
|
+
|
|
|
+ // 绑定视频元素
|
|
|
+ this.flvPlayer.attachMediaElement(videoElement);
|
|
|
+
|
|
|
+ // 加载视频
|
|
|
+ this.flvPlayer.load();
|
|
|
+
|
|
|
+ // 播放视频
|
|
|
+ this.flvPlayer.play().catch(e => {
|
|
|
+ console.error('FLV 播放错误:', e);
|
|
|
+ this.playStatus = 'error';
|
|
|
+ this.errorMessage = `FLV 播放错误: ${e.message || '未知错误'}`;
|
|
|
+ });
|
|
|
},
|
|
|
|
|
|
+ // 使用 HLS.js 播放 HLS/M3U8 视频
|
|
|
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
|
|
|
- });
|
|
|
+ if (!Hls.isSupported()) {
|
|
|
+ // 如果浏览器原生支持 HLS
|
|
|
+ if (videoElement.canPlayType('application/vnd.apple.mpegurl')) {
|
|
|
+ videoElement.src = this.videoUrl;
|
|
|
+ videoElement.load();
|
|
|
+ videoElement.play().catch(e => {
|
|
|
+ console.error('HLS 原生播放错误:', e);
|
|
|
+ this.playStatus = 'error';
|
|
|
+ this.errorMessage = `HLS 原生播放错误: ${e.message || '未知错误'}`;
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.playStatus = 'error';
|
|
|
+ this.errorMessage = '浏览器不支持 HLS 播放';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 销毁之前的实例
|
|
|
+ if (this.hlsPlayer) {
|
|
|
+ this.hlsPlayer.destroy();
|
|
|
+ this.hlsPlayer = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建 HLS 播放器
|
|
|
+ this.hlsPlayer = new Hls({
|
|
|
+ debug: false,
|
|
|
+ defaultAudioCodec: 'aac',
|
|
|
+ progressive: true,
|
|
|
+ lowLatencyMode: true,
|
|
|
+ backBufferLength: 90
|
|
|
+ });
|
|
|
+
|
|
|
+ // 绑定事件
|
|
|
+ this.hlsPlayer.on(Hls.Events.MEDIA_ATTACHED, () => {
|
|
|
+ console.log('HLS 媒体已附加');
|
|
|
this.hlsPlayer.loadSource(this.videoUrl);
|
|
|
- this.hlsPlayer.attachMedia(videoElement);
|
|
|
- this.hlsPlayer.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
|
- videoElement.play();
|
|
|
+ });
|
|
|
+
|
|
|
+ this.hlsPlayer.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
|
+ console.log('HLS 清单已解析');
|
|
|
+ videoElement.play().catch(e => {
|
|
|
+ console.error('HLS 播放错误:', e);
|
|
|
+ this.playStatus = 'error';
|
|
|
+ this.errorMessage = `HLS 播放错误: ${e.message || '未知错误'}`;
|
|
|
});
|
|
|
-
|
|
|
- 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;
|
|
|
- }
|
|
|
+ });
|
|
|
+
|
|
|
+ this.hlsPlayer.on(Hls.Events.ERROR, (event, data) => {
|
|
|
+ console.error('HLS 错误:', data);
|
|
|
+ if (data.fatal) {
|
|
|
+ switch(data.type) {
|
|
|
+ case Hls.ErrorTypes.NETWORK_ERROR:
|
|
|
+ // 尝试恢复网络错误
|
|
|
+ console.log('HLS 网络错误,尝试恢复...');
|
|
|
+ this.hlsPlayer.startLoad();
|
|
|
+ break;
|
|
|
+ case Hls.ErrorTypes.MEDIA_ERROR:
|
|
|
+ // 尝试恢复媒体错误
|
|
|
+ console.log('HLS 媒体错误,尝试恢复...');
|
|
|
+ this.hlsPlayer.recoverMediaError();
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ // 无法恢复的错误
|
|
|
+ this.playStatus = 'error';
|
|
|
+ this.errorMessage = `HLS 错误: ${data.details}`;
|
|
|
+ this.hlsPlayer.destroy();
|
|
|
+ break;
|
|
|
}
|
|
|
- });
|
|
|
- } else {
|
|
|
- alert('您的浏览器不支持HLS视频播放');
|
|
|
- }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 附加到视频元素
|
|
|
+ this.hlsPlayer.attachMedia(videoElement);
|
|
|
},
|
|
|
|
|
|
+ // 播放 MP4 视频
|
|
|
loadMp4Video(videoElement) {
|
|
|
+ // MP4 可以直接使用 video 元素播放
|
|
|
videoElement.src = this.videoUrl;
|
|
|
videoElement.load();
|
|
|
- videoElement.play();
|
|
|
+ videoElement.play().catch(e => {
|
|
|
+ console.error('MP4 播放错误:', e);
|
|
|
+ this.playStatus = 'error';
|
|
|
+ this.errorMessage = `MP4 播放错误: ${e.message || '未知错误'}`;
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 通过 WebSocket 播放视频
|
|
|
+ loadWebSocketVideo(videoElement) {
|
|
|
+ // 确保 URL 是 WebSocket 格式
|
|
|
+ let wsUrl = this.videoUrl;
|
|
|
+ if (!wsUrl.startsWith('ws://') && !wsUrl.startsWith('wss://')) {
|
|
|
+ wsUrl = `ws://${wsUrl.replace(/^https?:\/\//, '')}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 销毁之前的连接
|
|
|
+ if (this.wsConnection) {
|
|
|
+ this.wsConnection.close();
|
|
|
+ this.wsConnection = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建 MediaSource
|
|
|
+ this.mediaSource = new MediaSource();
|
|
|
+ videoElement.src = URL.createObjectURL(this.mediaSource);
|
|
|
+
|
|
|
+ this.mediaSource.addEventListener('sourceopen', () => {
|
|
|
+ console.log('MediaSource 已打开');
|
|
|
+
|
|
|
+ // 创建 SourceBuffer
|
|
|
+ try {
|
|
|
+ this.sourceBuffer = this.mediaSource.addSourceBuffer('video/mp4; codecs="avc1.42E01E, mp4a.40.2"');
|
|
|
+ } catch (e) {
|
|
|
+ console.error('创建 SourceBuffer 失败:', e);
|
|
|
+ this.playStatus = 'error';
|
|
|
+ this.errorMessage = `创建 SourceBuffer 失败: ${e.message}`;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建 WebSocket 连接
|
|
|
+ this.wsConnection = new WebSocket(wsUrl);
|
|
|
+
|
|
|
+ // 设置二进制类型
|
|
|
+ this.wsConnection.binaryType = 'arraybuffer';
|
|
|
+
|
|
|
+ // 连接打开
|
|
|
+ this.wsConnection.onopen = () => {
|
|
|
+ console.log('WebSocket 已连接');
|
|
|
+ };
|
|
|
+
|
|
|
+ // 接收数据
|
|
|
+ this.wsConnection.onmessage = (event) => {
|
|
|
+ if (this.sourceBuffer && !this.sourceBuffer.updating) {
|
|
|
+ try {
|
|
|
+ this.sourceBuffer.appendBuffer(event.data);
|
|
|
+ } catch (e) {
|
|
|
+ console.error('添加数据到 SourceBuffer 失败:', e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 连接错误
|
|
|
+ this.wsConnection.onerror = (e) => {
|
|
|
+ console.error('WebSocket 错误:', e);
|
|
|
+ this.playStatus = 'error';
|
|
|
+ this.errorMessage = 'WebSocket 连接错误';
|
|
|
+ };
|
|
|
+
|
|
|
+ // 连接关闭
|
|
|
+ this.wsConnection.onclose = () => {
|
|
|
+ console.log('WebSocket 已关闭');
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ // 播放视频
|
|
|
+ videoElement.play().catch(e => {
|
|
|
+ console.error('WebSocket 视频播放错误:', e);
|
|
|
+ this.playStatus = 'error';
|
|
|
+ this.errorMessage = `WebSocket 视频播放错误: ${e.message || '未知错误'}`;
|
|
|
+ });
|
|
|
},
|
|
|
|
|
|
+ // 销毁播放器实例
|
|
|
destroyPlayer() {
|
|
|
+ // 销毁 Jessibuca 播放器
|
|
|
+ if (this.jessibucaPlayer) {
|
|
|
+ this.jessibucaPlayer.destroy();
|
|
|
+ this.jessibucaPlayer = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 销毁 FLV 播放器
|
|
|
if (this.flvPlayer) {
|
|
|
- this.flvPlayer.unload();
|
|
|
- this.flvPlayer.detachMediaElement();
|
|
|
this.flvPlayer.destroy();
|
|
|
this.flvPlayer = null;
|
|
|
}
|
|
|
|
|
|
+ // 销毁 HLS 播放器
|
|
|
if (this.hlsPlayer) {
|
|
|
this.hlsPlayer.destroy();
|
|
|
this.hlsPlayer = null;
|
|
|
}
|
|
|
|
|
|
+ // 销毁 VideoJS 播放器
|
|
|
if (this.vjsPlayer) {
|
|
|
this.vjsPlayer.dispose();
|
|
|
this.vjsPlayer = null;
|
|
|
}
|
|
|
|
|
|
+ // 关闭 WebSocket 连接
|
|
|
+ if (this.wsConnection) {
|
|
|
+ this.wsConnection.close();
|
|
|
+ this.wsConnection = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 释放 MediaSource
|
|
|
+ if (this.mediaSource && this.mediaSource.readyState === 'open') {
|
|
|
+ try {
|
|
|
+ this.mediaSource.endOfStream();
|
|
|
+ } catch (e) {
|
|
|
+ console.error('关闭 MediaSource 失败:', e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.mediaSource = null;
|
|
|
+ this.sourceBuffer = null;
|
|
|
+
|
|
|
+ // 重置视频元素
|
|
|
const videoElement = this.$refs.videoElement;
|
|
|
if (videoElement) {
|
|
|
- // 移除事件监听器
|
|
|
- videoElement.removeEventListener('playing', this.handlePlaying);
|
|
|
- videoElement.removeEventListener('pause', this.handlePause);
|
|
|
- videoElement.removeEventListener('waiting', this.handleWaiting);
|
|
|
- videoElement.removeEventListener('error', this.handleError);
|
|
|
-
|
|
|
videoElement.src = '';
|
|
|
- videoElement.removeAttribute('src');
|
|
|
+ videoElement.load();
|
|
|
}
|
|
|
-
|
|
|
- // 重置播放状态
|
|
|
- this.playStatus = 'idle';
|
|
|
- this.errorMessage = '';
|
|
|
}
|
|
|
},
|
|
|
beforeDestroy() {
|
|
|
+ // 组件销毁前清理资源
|
|
|
this.destroyPlayer();
|
|
|
}
|
|
|
}
|
|
|
@@ -541,151 +642,175 @@ export default {
|
|
|
width: 100%;
|
|
|
max-width: 1200px;
|
|
|
margin: 0 auto;
|
|
|
- font-family: 'Roboto', sans-serif;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
+ padding: 20px;
|
|
|
+ box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
.video-wrapper {
|
|
|
- width: 100%;
|
|
|
position: relative;
|
|
|
+ width: 100%;
|
|
|
background-color: #000;
|
|
|
- border-radius: 4px 4px 0 0;
|
|
|
+ aspect-ratio: 16/9;
|
|
|
overflow: hidden;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
|
}
|
|
|
|
|
|
-.video-player {
|
|
|
+.video-player,
|
|
|
+.jessibuca-container {
|
|
|
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);
|
|
|
+ margin-top: 20px;
|
|
|
+ padding: 15px;
|
|
|
+ background-color: #f5f5f5;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
|
}
|
|
|
|
|
|
.format-decoder-options {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
- justify-content: space-between;
|
|
|
- margin-bottom: 16px;
|
|
|
+ gap: 20px;
|
|
|
+ margin-bottom: 20px;
|
|
|
}
|
|
|
|
|
|
-.format-selector {
|
|
|
- display: flex;
|
|
|
- gap: 16px;
|
|
|
- margin-bottom: 8px;
|
|
|
-}
|
|
|
-
|
|
|
-.format-label {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- cursor: pointer;
|
|
|
- font-size: 14px;
|
|
|
- color: #424242;
|
|
|
+.option-group {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 200px;
|
|
|
}
|
|
|
|
|
|
-.format-label input {
|
|
|
- margin-right: 6px;
|
|
|
+.option-title {
|
|
|
+ font-weight: bold;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ color: #333;
|
|
|
}
|
|
|
|
|
|
+.player-selector,
|
|
|
+.format-selector,
|
|
|
.decoder-options {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
gap: 10px;
|
|
|
}
|
|
|
|
|
|
+.player-label,
|
|
|
+.format-label,
|
|
|
.decoder-label {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
+ padding: 6px 12px;
|
|
|
+ background-color: #fff;
|
|
|
+ border-radius: 4px;
|
|
|
cursor: pointer;
|
|
|
- font-size: 14px;
|
|
|
- color: #424242;
|
|
|
- margin-right: 10px;
|
|
|
+ transition: all 0.2s;
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
}
|
|
|
|
|
|
+.player-label:hover,
|
|
|
+.format-label:hover,
|
|
|
+.decoder-label:hover {
|
|
|
+ background-color: #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.player-label input,
|
|
|
+.format-label input,
|
|
|
.decoder-label input {
|
|
|
margin-right: 6px;
|
|
|
}
|
|
|
|
|
|
.video-controls {
|
|
|
display: flex;
|
|
|
- gap: 12px;
|
|
|
- width: 100%;
|
|
|
+ gap: 10px;
|
|
|
}
|
|
|
|
|
|
.video-url-input {
|
|
|
flex: 1;
|
|
|
- padding: 12px;
|
|
|
+ padding: 10px 15px;
|
|
|
+ border: 1px solid #ddd;
|
|
|
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;
|
|
|
+ padding: 10px 20px;
|
|
|
+ background-color: #4CAF50;
|
|
|
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;
|
|
|
+ font-weight: bold;
|
|
|
+ transition: background-color 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.load-button:hover {
|
|
|
+ background-color: #45a049;
|
|
|
+}
|
|
|
+
|
|
|
+.button-text {
|
|
|
+ display: inline-block;
|
|
|
+}
|
|
|
+
|
|
|
+/* 播放状态指示器样式 */
|
|
|
+.play-status-overlay {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
|
- white-space: nowrap;
|
|
|
+ background-color: rgba(0, 0, 0, 0.5);
|
|
|
+ z-index: 10;
|
|
|
}
|
|
|
|
|
|
-.button-text {
|
|
|
- margin-left: 4px;
|
|
|
+.status-indicator {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 20px;
|
|
|
+ background-color: rgba(0, 0, 0, 0.7);
|
|
|
+ border-radius: 8px;
|
|
|
+ color: white;
|
|
|
}
|
|
|
|
|
|
-.load-button:hover {
|
|
|
- background-color: #7c4dff;
|
|
|
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
|
+.loading-spinner,
|
|
|
+.buffering-spinner {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ border: 4px solid rgba(255, 255, 255, 0.3);
|
|
|
+ border-top: 4px solid white;
|
|
|
+ border-radius: 50%;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+.error-icon {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background-color: #f44336;
|
|
|
+ color: white;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: bold;
|
|
|
+ margin-bottom: 10px;
|
|
|
}
|
|
|
|
|
|
-.load-button:active {
|
|
|
- background-color: #5600e8;
|
|
|
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
|
+.status-text {
|
|
|
+ font-size: 16px;
|
|
|
+ text-align: center;
|
|
|
}
|
|
|
|
|
|
-@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%;
|
|
|
- }
|
|
|
+@keyframes spin {
|
|
|
+ 0% { transform: rotate(0deg); }
|
|
|
+ 100% { transform: rotate(360deg); }
|
|
|
}
|
|
|
-</style>
|
|
|
+</style>
|