Pārlūkot izejas kodu

集成jeccibuca播放器

sangkf 7 mēneši atpakaļ
vecāks
revīzija
16566c3544

+ 30 - 0
copy-decoder-files.js

@@ -0,0 +1,30 @@
+const fs = require('fs');
+const path = require('path');
+
+// 源文件路径
+const sourceDir = path.join(__dirname, 'src/assets/jessibuca');
+// 目标路径
+const targetDir = path.join(__dirname, 'public');
+
+// 确保目标目录存在
+if (!fs.existsSync(targetDir)) {
+  fs.mkdirSync(targetDir, { recursive: true });
+}
+
+// 要复制的文件
+const filesToCopy = ['decoder.js', 'decoder.wasm'];
+
+// 复制文件
+filesToCopy.forEach(file => {
+  const sourcePath = path.join(sourceDir, file);
+  const targetPath = path.join(targetDir, file);
+  
+  if (fs.existsSync(sourcePath)) {
+    fs.copyFileSync(sourcePath, targetPath);
+    console.log(`已复制 ${file} 到 public 目录`);
+  } else {
+    console.error(`源文件 ${sourcePath} 不存在`);
+  }
+});
+
+console.log('解码器文件复制完成!');

+ 19 - 0
package-lock.json

@@ -1378,6 +1378,11 @@
         "@types/node": "*"
       }
     },
+    "@types/dom-webcodecs": {
+      "version": "0.1.14",
+      "resolved": "https://registry.npmmirror.com/@types/dom-webcodecs/-/dom-webcodecs-0.1.14.tgz",
+      "integrity": "sha512-ba9aF0qARLLQpLihONIRbj8VvAdUxO+5jIxlscVcDAQTcJmq5qVr781+ino5qbQUJUmO21cLP2eLeXYWzao5Vg=="
+    },
     "@types/eslint": {
       "version": "9.6.1",
       "resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-9.6.1.tgz",
@@ -1577,6 +1582,11 @@
         "@types/node": "*"
       }
     },
+    "@vicons/antd": {
+      "version": "0.13.0",
+      "resolved": "https://registry.npmmirror.com/@vicons/antd/-/antd-0.13.0.tgz",
+      "integrity": "sha512-yrUGoUSz2BbGupk9ghQOahc04n5H3MwUDM9pVPsLh9U1uqB47oRWZvYRiZaT1JKPqgTgSE6BXcVw4i9MOF4M+g=="
+    },
     "@videojs/http-streaming": {
       "version": "2.16.3",
       "resolved": "https://registry.npmmirror.com/@videojs/http-streaming/-/http-streaming-2.16.3.tgz",
@@ -5017,6 +5027,15 @@
       "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==",
       "dev": true
     },
+    "jessibuca": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/jessibuca/-/jessibuca-4.0.0.tgz",
+      "integrity": "sha512-BQw6mmz+hQB+VlDqiAvL1a5MmJFDj/YLzBGfFVuSR6wh8ZgcqkUMwZkRrWByK4rY9rQgPrsO+4BJ9HxO1UyZ4Q==",
+      "requires": {
+        "@types/dom-webcodecs": "^0.1.8",
+        "@vicons/antd": "^0.13.0"
+      }
+    },
     "jest-worker": {
       "version": "27.5.1",
       "resolved": "https://registry.npmmirror.com/jest-worker/-/jest-worker-27.5.1.tgz",

+ 10 - 12
package.json

@@ -3,9 +3,10 @@
   "version": "0.1.0",
   "private": true,
   "scripts": {
-    "serve": "vue-cli-service serve",
-    "build": "vue-cli-service build",
-    "lint": "vue-cli-service lint"
+    "serve": "node copy-decoder-files.js && vue-cli-service serve",
+    "build": "node copy-decoder-files.js && vue-cli-service build",
+    "lint": "vue-cli-service lint",
+    "copy-decoder": "node copy-decoder-files.js"
   },
   "dependencies": {
     "core-js": "^3.8.3",
@@ -20,15 +21,12 @@
   "devDependencies": {
     "@babel/core": "^7.12.16",
     "@babel/eslint-parser": "^7.12.16",
-    "@vue/cli-plugin-babel": "^4.0.4",
-    "@vue/cli-plugin-eslint": "^4.0.4",
-    "@vue/cli-plugin-router": "^4.0.4",
-    "@vue/cli-plugin-unit-jest": "^4.0.4",
-    "@vue/cli-plugin-vuex": "^4.0.4",
-    "@vue/cli-service": "^4.0.4",
-    "eslint": "^6.8.0",
-    "eslint-plugin-vue": "^5.2.3",
-    "vue-template-compiler": "2.6.10"
+    "@vue/cli-plugin-babel": "~5.0.0",
+    "@vue/cli-plugin-eslint": "~5.0.0",
+    "@vue/cli-service": "~5.0.0",
+    "eslint": "^7.32.0",
+    "eslint-plugin-vue": "^8.0.3",
+    "vue-template-compiler": "^2.6.14"
   },
   "eslintConfig": {
     "root": true,

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
public/decoder.js


BIN
public/decoder.wasm


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
src/assets/EasyWasmPlayer.js


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
src/assets/jessibuca/decoder.js


BIN
src/assets/jessibuca/decoder.wasm


+ 637 - 0
src/assets/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;

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 4930 - 0
src/assets/jessibuca/jessibuca.js


+ 556 - 431
src/components/VideoPlayer.vue

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

+ 97 - 0
src/utils/jessibucaConfig.js

@@ -0,0 +1,97 @@
+/**
+ * Jessibuca 播放器配置
+ */
+export const getJessibucaConfig = (container) => {
+  return {
+    container, // 播放器容器
+    videoBuffer: 0.5, // 视频缓冲区时长,单位:秒
+    audioBuffer: 0.5, // 音频缓冲区时长,单位:秒
+    debug: true, // 是否开启调试模式
+    forceNoOffscreen: true, // 是否强制不使用离屏模式
+    isResize: true, // 是否根据容器自动调整大小
+    isFullResize: false, // 是否全屏模式调整大小
+    text: '视频加载中...', // 加载过程中的提示文本
+    loadingText: '加载中...',
+    background: '#000000', // 背景色
+    controlAutoHide: false, // 控制栏是否自动隐藏
+    showBandwidth: true, // 是否显示网速
+    operateBtns: {
+      fullscreen: true, // 是否显示全屏按钮
+      screenshot: true, // 是否显示截图按钮
+      play: true, // 是否显示播放按钮
+      audio: true, // 是否显示声音按钮
+      recording: false // 是否显示录制按钮
+    },
+    supportDblclickFullscreen: true, // 是否支持双击全屏
+    showBandwidthPosition: 'controls', // 显示网速的位置
+    keepScreenOn: true, // 是否保持屏幕常亮
+    hotKey: true, // 是否支持热键
+    loadingTimeout: 10, // 视频加载超时时间,单位:秒
+    // 指定解码器文件路径
+    decoder: process.env.BASE_URL + 'decoder.js',
+    wasmUrl: process.env.BASE_URL + 'decoder.wasm',
+    
+    // 解码配置
+    decoderConfig: {
+      // 解码类型,可选值:'software', 'hardware'
+      type: 'auto',
+      // 是否优先使用 WASM 解码
+      preferWasm: true,
+      // 是否使用 Worker 进行解码
+      useWorker: true,
+      // 是否使用 SIMD 加速
+      useSIMD: true,
+      // 是否使用 WebGL 渲染
+      useWebGL: true,
+    },
+    
+    // 视频渲染配置
+    render: {
+      // 是否开启抗锯齿
+      antialias: true,
+      // 是否使用 WebGL2
+      useWebGL2: true,
+      // 是否使用 YUV 渲染
+      yuv: true,
+    },
+    
+    // 网络配置
+    network: {
+      // 是否开启 HTTP 请求超时
+      timeout: 10000,
+      // 是否开启重试
+      retry: true,
+      // 重试次数
+      retryCount: 3,
+      // 重试间隔,单位:毫秒
+      retryDelay: 1000,
+    }
+  };
+};
+
+/**
+ * 根据视频格式获取 Jessibuca 播放地址
+ * @param {string} url - 原始 URL
+ * @param {string} format - 视频格式
+ * @returns {string} - 处理后的 URL
+ */
+export const getJessibucaUrl = (url, format) => {
+  // 对于 WebSocket 格式,确保 URL 以 ws:// 或 wss:// 开头
+  if (format === 'ws' && !url.startsWith('ws://') && !url.startsWith('wss://')) {
+    return `ws://${url.replace(/^https?:\/\//, '')}`;
+  }
+  
+  // 对于 FLV 格式,确保 URL 正确
+  if (format === 'flv' && !url.includes('.flv') && !url.includes('?') && !url.includes('=')) {
+    // 如果 URL 不包含 .flv 后缀且没有查询参数,尝试添加 .flv 后缀
+    return `${url}.flv`;
+  }
+  
+  // 对于 HLS/M3U8 格式,确保 URL 正确
+  if ((format === 'hls' || format === 'm3u8') && !url.includes('.m3u8') && !url.includes('?') && !url.includes('=')) {
+    // 如果 URL 不包含 .m3u8 后缀且没有查询参数,尝试添加 .m3u8 后缀
+    return `${url}.m3u8`;
+  }
+  
+  return url;
+};

+ 0 - 3
src/views/HomePage.vue

@@ -7,9 +7,6 @@
     <main class="page-content">
       <VideoPlayer />
     </main>
-    <footer class="page-footer">
-      <p>基于 Vue.js 和 flv.js 构建的 Material Design 风格视频播放器</p>
-    </footer>
   </div>
 </template>
 

+ 3 - 1
vue.config.js

@@ -1,4 +1,6 @@
 const { defineConfig } = require('@vue/cli-service')
 module.exports = defineConfig({
-  transpileDependencies: true
+  transpileDependencies: true,
+  // 关闭 ESLint 检查
+  lintOnSave: false
 })

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels