添加播放器
This commit is contained in:
parent
d7436e0229
commit
8e064cb01a
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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.
|
|
@ -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(默认),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,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 留给上层用户操作来触发音频恢复的方法。
|
||||||
|
*
|
||||||
|
* 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('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(默认) ,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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换底部控制条 隐藏/显示
|
||||||
|
* @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
|
|
@ -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>
|
|
||||||
|
// 播放测试 - FLV和HLS
|
||||||
|
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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue