添加播放器

This commit is contained in:
孙小云 2025-12-10 13:57:26 +08:00
parent d7436e0229
commit 8e064cb01a
14 changed files with 17257 additions and 3 deletions

View File

@ -7,6 +7,12 @@
<title>ThingsBoard WebSocket Demo</title> <title>ThingsBoard WebSocket Demo</title>
</head> </head>
<body> <body>
<!-- 视频播放器库 -->
<script type="text/javascript" src="/static/js/jessibuca/jessibuca.js"></script>
<script type="text/javascript" src="/static/js/ZLMRTCClient.js"></script>
<script type="text/javascript" src="/static/js/h265web/h265webjs-v20221106.js"></script>
<script type="text/javascript" src="/static/js/h265web/missile.js"></script>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>

30
package-lock.json generated
View File

@ -8,6 +8,8 @@
"name": "thingsboard-html-demo", "name": "thingsboard-html-demo",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"flv.js": "^1.6.2",
"hls.js": "^1.6.15",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
@ -2978,6 +2980,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmmirror.com/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
"license": "MIT"
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.21.5", "version": "0.21.5",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz",
@ -3458,6 +3466,16 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/flv.js": {
"version": "1.6.2",
"resolved": "https://registry.npmmirror.com/flv.js/-/flv.js-1.6.2.tgz",
"integrity": "sha512-xre4gUbX1MPtgQRKj2pxJENp/RnaHaxYvy3YToVVCrSmAWUu85b9mug6pTXF6zakUjNP2lFWZ1rkSX7gxhB/2A==",
"license": "Apache-2.0",
"dependencies": {
"es6-promise": "^4.2.8",
"webworkify-webpack": "^2.1.5"
}
},
"node_modules/foreground-child": { "node_modules/foreground-child": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz",
@ -3749,6 +3767,12 @@
"he": "bin/he" "he": "bin/he"
} }
}, },
"node_modules/hls.js": {
"version": "1.6.15",
"resolved": "https://registry.npmmirror.com/hls.js/-/hls.js-1.6.15.tgz",
"integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==",
"license": "Apache-2.0"
},
"node_modules/html-encoding-sniffer": { "node_modules/html-encoding-sniffer": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", "resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
@ -6009,6 +6033,12 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/webworkify-webpack": {
"version": "2.1.5",
"resolved": "https://registry.npmmirror.com/webworkify-webpack/-/webworkify-webpack-2.1.5.tgz",
"integrity": "sha512-2akF8FIyUvbiBBdD+RoHpoTbHMQF2HwjcxfDvgztAX5YwbZNyrtfUMgvfgFVsgDhDPVTlkbb5vyasqDHfIDPQw==",
"license": "MIT"
},
"node_modules/whatwg-encoding": { "node_modules/whatwg-encoding": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "resolved": "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",

View File

@ -13,6 +13,8 @@
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"flv.js": "^1.6.2",
"hls.js": "^1.6.15",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-router": "^4.3.0" "vue-router": "^4.3.0"

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

97
public/static/js/h265web/index.d.ts vendored Normal file
View File

@ -0,0 +1,97 @@
export interface Web265JsExtraConfig {
moovStartFlag?: boolean
rawFps?: number
autoCrop?: boolean
core?: 0 | 1
coreProbePart?: number
ignoreAudio?: 0 | 1
probeSize?: number
}
export interface Web265JsConfig {
/**
*The type of the file to be played, do not fill in the automatic identification
*/
type?: 'mp4' | 'hls' | 'ts' | 'raw265' | 'flv'
/**
* playback window dom id value
*/
player: string
/**
* the video window width size
*/
width: number
/**
* the video window height size
*/
height: number
/**
* player token value
*/
token: string
extInfo?: Web265JsExtraConfig
}
export interface Web265JsMediaInfo {
audioNone: boolean
durationMs: number
fps: number
sampleRate: number
size: {
height: number
width: number
}
videoCodec: 0 | 1
isHEVC: boolean
videoType: Web265JsConfig['type']
}
interface New265WebJs {
onSeekFinish(): void
onRender(
width: number,
height: number,
imageBufferY: typeof Uint8Array,
imageBufferB: typeof Uint8Array,
imageBufferR: typeof Uint8Array
): void
onLoadFinish(): void
onPlayTime(videoPTS: number): void
onPlayFinish(): void
onCacheProcess(cPts: number): void
onReadyShowDone(): void
onLoadCache(): void
onLoadCacheFinshed(): void
onOpenFullScreen(): void
onCloseFullScreen(): void
do(): void
pause(): void
isPlaying(): boolean
setRenderScreen(state: boolean): void
seek(pts: number): void
setVoice(volume: number): void
mediaInfo(): Web265JsMediaInfo
fullScreen(): void
closeFullScreen(): void
playNextFrame(): void
snapshot(): void
release(): void
setPlaybackRate(rate: number): void
getPlaybackRate(): number
}
declare type new265webJsFn = (
url: string,
config: Web265JsConfig
) => New265WebJs
declare global {
interface Window {
new265webjs: new265webJsFn
}
}
export default class H265webjsModule {
static createPlayer: (url: string, config: Web265JsConfig) => New265WebJs
static clear(): void
}

View File

@ -0,0 +1,32 @@
/*********************************************************
* LICENSE: LICENSE-Free_CN.MD
*
* Author: Numberwolf - ChangYanlong
* QQ: 531365872
* QQ Group:925466059
* Wechat: numberwolf11
* Discord: numberwolf#8694
* E-Mail: porschegt23@foxmail.com
* Github: https://github.com/numberwolf/h265web.js
*
* 作者: 小老虎(Numberwolf)(常炎隆)
* QQ: 531365872
* QQ群: 531365872
* 微信: numberwolf11
* Discord: numberwolf#8694
* 邮箱: porschegt23@foxmail.com
* 博客: https://www.jianshu.com/u/9c09c1e00fd1
* Github: https://github.com/numberwolf/h265web.js
*
**********************************************************/
require('./h265webjs-v20221106');
export default class h265webjs {
static createPlayer(videoURL, config) {
return window.new265webjs(videoURL, config);
}
static clear() {
global.STATICE_MEM_playerCount = -1;
global.STATICE_MEM_playerIndexPtr = 0;
}
}

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -0,0 +1,683 @@
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()180270
*/
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.264Safari on iOS不支持
* * forceNoOffscreen false ()
*/
useMSE?: boolean;
/**
* Webcodecs硬解码
* * H.264 (chrome 94https或者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分辨率下的UV分量宽度是540/2=2704绿
*/
openWebglAlignment?: boolean,
/**
* webcodecs硬解码是否通过video标签渲染
*/
wcsUseVideoRender?: boolean,
/**
*
*/
controlAutoHide?: boolean,
/**
*
*/
recordType?: 'webm' | 'mp4',
/**
* 使web全屏(90)
*/
useWebFullScreen?: boolean,
/**
* 使
*/
autoUseSystemFullScreen?: 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;
/**
*
*
* iPhonechrome等要求自动播放时使
*
* 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('url',{headers:{'Authorization':'test111'}}).then(()=>{
console.log('play success')
}).catch((e)=>{
console.log('play error',e)
})
*/
play(url?: string, options?: {
headers: Object
}): Promise<void>;
/**
*
*/
resize(): void;
/**
*
*
* `videoBuffer`
*
@example
// 设置 200ms 缓冲
jessibuca.setBufferTime(0.2)
*/
setBufferTime(time: number): void;
/**
* 0() 180270
*
* > 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 84API, 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 webmwebm 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;
/**
* /
* @param isShow
*
* @example
* jessibuca.toggleControlBar(true) // 显示
* jessibuca.toggleControlBar(false) // 隐藏
* jessibuca.toggleControlBar() // 切换 隐藏/显示
*/
toggleControlBar(isShow:boolean): void;
/**
*
*/
getControlBarShow(): 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;

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, onUnmounted } from 'vue'
import Hls from 'hls.js'
import flvjs from 'flv.js'
const GATEWAY_URL = 'http://127.0.0.1:8080' const GATEWAY_URL = 'http://localhost:8080'
// getPushList // getPushList
const pushPage = ref(1) const pushPage = ref(1)
@ -80,7 +82,380 @@ const getPushStart = async () => {
startLoading.value = false startLoading.value = false
} }
} }
</script>
// - FLVHLS
const flvUrl = ref('http://localhost:8080/zlm/live/prod.live.flv?originTypeStr=rtmp_push&audioCodec=AAC&videoCodec=H264')
const hlsUrl = ref('http://localhost:8080/zlm/live/prod/hls.m3u8?originTypeStr=rtmp_push&audioCodec=AAC&videoCodec=H264')
const testFlvError = ref('')
const testHlsError = ref('')
// FLV
let flvPlayer: any = null
const flvVideoRef = ref<HTMLVideoElement | null>(null)
const isFlvPlaying = ref(false)
const playFlv = () => {
testFlvError.value = ''
if (!flvVideoRef.value) {
testFlvError.value = '视频元素未找到'
return
}
if (flvPlayer) {
flvPlayer.destroy()
flvPlayer = null
}
if (flvjs.isSupported()) {
try {
flvPlayer = flvjs.createPlayer({
type: 'flv',
url: flvUrl.value,
isLive: true,
hasAudio: true,
hasVideo: true,
withCredentials: true
})
flvPlayer.attachMediaElement(flvVideoRef.value)
flvPlayer.load()
flvPlayer.play()
isFlvPlaying.value = true
flvPlayer.on(flvjs.Events.ERROR, (errorType: string, errorDetail: string) => {
testFlvError.value = `播放错误: ${errorType} - ${errorDetail}`
isFlvPlaying.value = false
})
} catch (error) {
testFlvError.value = error instanceof Error ? error.message : '播放FLV失败'
isFlvPlaying.value = false
}
} else {
testFlvError.value = '当前浏览器不支持 FLV 播放'
}
}
const stopFlv = () => {
if (flvPlayer) {
flvPlayer.pause()
flvPlayer.unload()
flvPlayer.detachMediaElement()
flvPlayer.destroy()
flvPlayer = null
isFlvPlaying.value = false
}
}
// HLS
let hlsPlayer: Hls | null = null
const hlsVideoRef = ref<HTMLVideoElement | null>(null)
const isHlsPlaying = ref(false)
const playHls = () => {
testHlsError.value = ''
if (!hlsVideoRef.value) {
testHlsError.value = '视频元素未找到'
return
}
if (hlsPlayer) {
hlsPlayer.destroy()
hlsPlayer = null
}
if (Hls.isSupported()) {
try {
hlsPlayer = new Hls({
xhrSetup: (xhr) => {
xhr.withCredentials = true
}
})
hlsPlayer.loadSource(hlsUrl.value)
hlsPlayer.attachMedia(hlsVideoRef.value)
hlsPlayer.on(Hls.Events.MANIFEST_PARSED, () => {
hlsVideoRef.value?.play()
isHlsPlaying.value = true
})
hlsPlayer.on(Hls.Events.ERROR, (event, data) => {
if (data.fatal) {
testHlsError.value = `播放错误: ${data.type} - ${data.details}`
isHlsPlaying.value = false
}
})
} catch (error) {
testHlsError.value = error instanceof Error ? error.message : '播放HLS失败'
isHlsPlaying.value = false
}
} else if (hlsVideoRef.value.canPlayType('application/vnd.apple.mpegurl')) {
// Safari HLS
hlsVideoRef.value.src = hlsUrl.value
hlsVideoRef.value.play()
isHlsPlaying.value = true
} else {
testHlsError.value = '当前浏览器不支持 HLS 播放'
}
}
const stopHls = () => {
if (hlsPlayer) {
hlsPlayer.destroy()
hlsPlayer = null
}
if (hlsVideoRef.value) {
hlsVideoRef.value.pause()
hlsVideoRef.value.src = ''
}
isHlsPlaying.value = false
}
// WebRTC
let webrtcPlayer: any = null
const webrtcVideoRef = ref<HTMLVideoElement | null>(null)
const isWebrtcPlaying = ref(false)
const webrtcUrl = ref('http://localhost:8080/zlm/index/api/webrtc?app=live&stream=prod&type=play&originTypeStr=rtmp_push&audioCodec=AAC&videoCodec=H264')
const testWebrtcError = ref('')
const playWebrtc = () => {
testWebrtcError.value = ''
if (!webrtcVideoRef.value) {
testWebrtcError.value = '视频元素未找到'
return
}
if (webrtcPlayer) {
webrtcPlayer.close()
webrtcPlayer = null
}
try {
// @ts-ignore
webrtcPlayer = new window.ZLMRTCClient.Endpoint({
element: webrtcVideoRef.value,
debug: true,
zlmsdpUrl: webrtcUrl.value,
simulecast: false,
useCamera: false,
audioEnable: true,
videoEnable: true,
recvOnly: true,
usedatachannel: false
})
// @ts-ignore
webrtcPlayer.on(window.ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, () => {
testWebrtcError.value = 'ICE 协商出错'
isWebrtcPlaying.value = false
})
// @ts-ignore
webrtcPlayer.on(window.ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS, (e: any) => {
console.log('WebRTC 播放成功', e.streams)
isWebrtcPlaying.value = true
})
// @ts-ignore
webrtcPlayer.on(window.ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, (e: any) => {
testWebrtcError.value = `offer anwser 交换失败: ${e.msg || ''}`
isWebrtcPlaying.value = false
})
} catch (error) {
testWebrtcError.value = error instanceof Error ? error.message : '播放WebRTC失败'
isWebrtcPlaying.value = false
}
}
const stopWebrtc = () => {
if (webrtcPlayer) {
webrtcPlayer.close()
webrtcPlayer = null
}
isWebrtcPlaying.value = false
}
// Jessibuca
let jessibucaPlayer: any = null
const jessibucaContainerRef = ref<HTMLDivElement | null>(null)
const isJessibucaPlaying = ref(false)
const jessibucaUrl = ref('http://localhost:8080/zlm/live/prod.live.flv?originTypeStr=rtmp_push&audioCodec=AAC&videoCodec=H264')
const testJessibucaError = ref('')
const jessibucaKBps = ref(0)
const playJessibuca = () => {
testJessibucaError.value = ''
if (!jessibucaContainerRef.value) {
testJessibucaError.value = '容器元素未找到'
return
}
if (jessibucaPlayer) {
jessibucaPlayer.destroy()
jessibucaPlayer = null
}
try {
// @ts-ignore
jessibucaPlayer = new window.Jessibuca({
container: jessibucaContainerRef.value,
videoBuffer: 0,
isResize: false,
useMSE: true,
useWCS: false,
text: '',
controlAutoHide: false,
debug: true,
hotKey: true,
// @ts-ignore
decoder: '/static/js/jessibuca/decoder.js',
timeout: 10,
recordType: 'mp4',
isFlv: false,
forceNoOffscreen: true,
hasAudio: true,
heartTimeout: 5,
heartTimeoutReplay: true,
heartTimeoutReplayTimes: 3,
hiddenAutoPause: false,
isNotMute: true,
keepScreenOn: true,
loadingText: '请稍等, 视频加载中......',
loadingTimeout: 10,
loadingTimeoutReplay: true,
loadingTimeoutReplayTimes: 3,
operateBtns: {
fullscreen: false,
screenshot: false,
play: false,
audio: false,
recorder: false
},
showBandwidth: false,
supportDblclickFullscreen: false,
useWebFullSreen: true,
wasmDecodeErrorReplay: true
})
jessibucaPlayer.on('play', () => {
isJessibucaPlaying.value = true
})
jessibucaPlayer.on('pause', () => {
isJessibucaPlaying.value = false
})
jessibucaPlayer.on('kBps', (kBps: number) => {
jessibucaKBps.value = Math.round(kBps)
})
jessibucaPlayer.on('error', (error: any) => {
console.error('Jessibuca error:', error)
testJessibucaError.value = '播放错误: ' + (error.message || error)
isJessibucaPlaying.value = false
})
jessibucaPlayer.on('timeout', () => {
testJessibucaError.value = '播放超时'
isJessibucaPlaying.value = false
})
jessibucaPlayer.play(jessibucaUrl.value)
} catch (error) {
testJessibucaError.value = error instanceof Error ? error.message : '播放Jessibuca失败'
isJessibucaPlaying.value = false
}
}
const stopJessibuca = () => {
if (jessibucaPlayer) {
jessibucaPlayer.pause()
jessibucaPlayer.destroy()
jessibucaPlayer = null
}
isJessibucaPlaying.value = false
}
// H265Web
let h265webPlayer: any = null
const isH265webPlaying = ref(false)
const h265webUrl = ref('http://localhost:8080/zlm/live/prod.live.flv?originTypeStr=rtmp_push&audioCodec=AAC&videoCodec=H264')
const testH265webError = ref('')
const h265webLoading = ref(false)
const h265webPlayerId = 'h265webPlayerContainer'
const playH265web = () => {
testH265webError.value = ''
h265webLoading.value = true
const container = document.getElementById(h265webPlayerId)
if (!container) {
testH265webError.value = '容器元素未找到'
h265webLoading.value = false
return
}
if (h265webPlayer) {
h265webPlayer.release()
h265webPlayer = null
}
try {
const width = container.clientWidth || 640
const height = container.clientHeight || 360
// @ts-ignore
h265webPlayer = new window.new265webjs(h265webUrl.value, {
player: h265webPlayerId, // ID , DOM
width: width,
height: height,
token: 'base64:QXV0aG9yOmNoYW5neWFubG9uZ3xudW1iZXJ3b2xmLEdpdGh1YjpodHRwczovL2dpdGh1Yi5jb20vbnVtYmVyd29sZixFbWFpbDpwb3JzY2hlZ3QyM0Bmb3htYWlsLmNvbSxRUTo1MzEzNjU4NzIsSG9tZVBhZ2U6aHR0cDovL3h2aWRlby52aWRlbyxEaXNjb3JkOm51bWJlcndvbGYjODY5NCx3ZWNoYXI6bnVtYmVyd29sZjExLEJlaWppbmcsV29ya0luOkJhaWR1',
extInfo: {
coreProbePart: 0.4,
probeSize: 8192,
ignoreAudio: 0
}
})
h265webPlayer.onReadyShowDone = () => {
const result = h265webPlayer.play()
isH265webPlaying.value = result
h265webLoading.value = false
}
h265webPlayer.onLoadFinish = () => {
console.log('H265Web 加载完成')
}
h265webPlayer.do()
} catch (error) {
testH265webError.value = error instanceof Error ? error.message : '播放H265Web失败'
isH265webPlaying.value = false
h265webLoading.value = false
}
}
const stopH265web = () => {
if (h265webPlayer) {
h265webPlayer.release()
h265webPlayer = null
}
isH265webPlaying.value = false
h265webLoading.value = false
}
//
onUnmounted(() => {
stopFlv()
stopHls()
stopWebrtc()
stopJessibuca()
stopH265web()
})</script>
<template> <template>
<div class="wvp-container"> <div class="wvp-container">
@ -249,6 +624,205 @@ const getPushStart = async () => {
</div> </div>
</div> </div>
</div> </div>
<div class="api-section">
<h3>3. 播放地址测试 (FLV & HLS)</h3>
<div class="api-description">
<p>测试通过网关访问ZLM流媒体服务器的播放地址</p>
<p>这些地址可以直接在播放器中使用</p>
</div>
<div class="test-section">
<h4>HTTP-FLV 播放器</h4>
<div class="form-group">
<label>FLV 播放地址:</label>
<input
v-model="flvUrl"
type="text"
placeholder="http://localhost:8080/zlm/live/prod.live.flv"
/>
</div>
<div class="button-group">
<button @click="playFlv" :disabled="isFlvPlaying">
{{ isFlvPlaying ? '播放中...' : '开始播放' }}
</button>
<button @click="stopFlv" :disabled="!isFlvPlaying" class="stop-button">
停止播放
</button>
<a :href="flvUrl" target="_blank" class="link-button secondary">
在新窗口打开
</a>
</div>
<div class="video-container">
<video
ref="flvVideoRef"
controls
muted
class="video-player"
></video>
</div>
<div v-if="testFlvError" class="error">
错误: {{ testFlvError }}
</div>
</div>
<div class="test-section">
<h4>HLS (M3U8) 播放器</h4>
<div class="form-group">
<label>HLS 播放地址:</label>
<input
v-model="hlsUrl"
type="text"
placeholder="http://localhost:8080/zlm/live/prod/hls.m3u8"
/>
</div>
<div class="button-group">
<button @click="playHls" :disabled="isHlsPlaying">
{{ isHlsPlaying ? '播放中...' : '开始播放' }}
</button>
<button @click="stopHls" :disabled="!isHlsPlaying" class="stop-button">
停止播放
</button>
<a :href="hlsUrl" target="_blank" class="link-button secondary">
在新窗口打开
</a>
</div>
<div class="video-container">
<video
ref="hlsVideoRef"
controls
muted
class="video-player"
></video>
</div>
<div v-if="testHlsError" class="error">
错误: {{ testHlsError }}
</div>
</div>
<div class="test-section">
<h4>WebRTC 播放器 (低延迟)</h4>
<div class="form-group">
<label>WebRTC 播放地址:</label>
<input
v-model="webrtcUrl"
type="text"
placeholder="http://localhost:8080/zlm/index/api/webrtc?app=live&stream=prod&type=play"
/>
</div>
<div class="button-group">
<button @click="playWebrtc" :disabled="isWebrtcPlaying">
{{ isWebrtcPlaying ? '播放中...' : '开始播放' }}
</button>
<button @click="stopWebrtc" :disabled="!isWebrtcPlaying" class="stop-button">
停止播放
</button>
</div>
<div class="video-container">
<video
ref="webrtcVideoRef"
controls
autoplay
muted
class="video-player"
></video>
</div>
<div v-if="testWebrtcError" class="error">
错误: {{ testWebrtcError }}
</div>
</div>
<div class="test-section">
<h4>Jessibuca 播放器 (多功能)</h4>
<div class="form-group">
<label>Jessibuca 播放地址:</label>
<input
v-model="jessibucaUrl"
type="text"
placeholder="http://localhost:8080/zlm/live/prod.live.flv"
/>
</div>
<div class="button-group">
<button @click="playJessibuca" :disabled="isJessibucaPlaying">
{{ isJessibucaPlaying ? '播放中...' : '开始播放' }}
</button>
<button @click="stopJessibuca" :disabled="!isJessibucaPlaying" class="stop-button">
停止播放
</button>
<span v-if="isJessibucaPlaying" class="info-text">码率: {{ jessibucaKBps }} kb/s</span>
</div>
<div class="video-container">
<div
ref="jessibucaContainerRef"
class="video-player"
style="position: relative; min-height: 360px; height: 500px;"
></div>
</div>
<div v-if="testJessibucaError" class="error">
错误: {{ testJessibucaError }}
</div>
</div>
<div class="test-section">
<h4>H265Web 播放器 (H.265 编码)</h4>
<div class="form-group">
<label>H265Web 播放地址:</label>
<input
v-model="h265webUrl"
type="text"
placeholder="http://localhost:8080/zlm/live/prod.live.flv"
/>
</div>
<div class="button-group">
<button @click="playH265web" :disabled="isH265webPlaying || h265webLoading">
{{ h265webLoading ? '加载中...' : (isH265webPlaying ? '播放中...' : '开始播放') }}
</button>
<button @click="stopH265web" :disabled="!isH265webPlaying && !h265webLoading" class="stop-button">
停止播放
</button>
</div>
<div class="video-container">
<div
id="h265webPlayerContainer"
class="video-player"
style="position: relative; min-height: 360px;"
>
<div v-if="h265webLoading" class="play-loading">
<i class="el-icon-loading" />
视频加载中
</div>
</div>
</div>
<div v-if="testH265webError" class="error">
错误: {{ testH265webError }}
</div>
</div>
<div class="info-box">
<h4>播放器说明:</h4>
<p><strong>FLV (flv.js):</strong> 使用 flv.js 库播放 FLV 兼容性好</p>
<p><strong>HLS (hls.js):</strong> 使用 hls.js 库播放 M3U8 适合点播</p>
<p><strong>WebRTC:</strong> 超低延迟 (< 500ms)适合实时监控需要 HTTPS localhost</p>
<p><strong>Jessibuca:</strong> 功能强大的多格式播放器支持 FLV/HLS/WebSocket-FLV有心跳重连</p>
<p><strong>H265Web:</strong> 支持 H.265/HEVC 编码高清低码率需要 token 授权</p>
<p><strong>注意:</strong> 确保推流正在进行否则播放地址会返回404错误</p>
</div>
</div>
</div> </div>
</template> </template>
@ -457,4 +1031,120 @@ button:disabled {
font-size: 12px; font-size: 12px;
color: #d32f2f; color: #d32f2f;
} }
.test-section {
margin-top: 20px;
padding: 15px;
background-color: #fafafa;
border-radius: 4px;
border: 1px solid #e0e0e0;
}
.test-section h4 {
margin-top: 0;
margin-bottom: 15px;
color: #2196f3;
font-size: 16px;
}
.success {
margin-top: 15px;
padding: 12px;
background-color: #e8f5e9;
color: #2e7d32;
border-radius: 4px;
font-size: 14px;
white-space: pre-wrap;
}
.info-box {
margin-top: 20px;
padding: 15px;
background-color: #e3f2fd;
border-radius: 4px;
border-left: 4px solid #2196f3;
}
.info-box h4 {
margin-top: 0;
margin-bottom: 10px;
color: #1976d2;
}
.info-box p {
margin: 8px 0;
font-size: 14px;
color: #333;
}
.link-button {
display: inline-block;
padding: 10px 20px;
background-color: #2196f3;
color: white;
text-decoration: none;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
transition: background-color 0.3s ease;
text-align: center;
}
.link-button:hover {
background-color: #1976d2;
}
.link-button.secondary {
background-color: #4caf50;
}
.link-button.secondary:hover {
background-color: #45a049;
}
.stop-button {
background-color: #f44336;
}
.stop-button:hover:not(:disabled) {
background-color: #d32f2f;
}
.video-container {
margin-top: 20px;
background-color: #000;
border-radius: 4px;
overflow: hidden;
}
.video-player {
width: 100%;
max-height: 500px;
display: block;
background-color: #000;
}
.info-text {
color: #2196f3;
font-size: 14px;
font-weight: 500;
margin-left: 10px;
}
.play-loading {
width: 100%;
height: 100%;
min-height: 360px;
color: rgb(255, 255, 255);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
background-color: #000;
}
.play-loading i {
margin-right: 10px;
font-size: 24px;
}
</style> </style>