初始提交
This commit is contained in:
parent
0562dd9885
commit
53c5bdd28e
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
118
BUILD_TROUBLESHOOTING.md
Normal file
118
BUILD_TROUBLESHOOTING.md
Normal file
@ -0,0 +1,118 @@
|
||||
# ZAPlayer 构建故障排除指南
|
||||
|
||||
## 常见问题解决
|
||||
|
||||
### 1. Terser 依赖错误
|
||||
|
||||
**错误信息:**
|
||||
```
|
||||
error during build: [vite:terser] terser not found. Since Vite v3, terser has become an optional dependency. You need to install it.
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
|
||||
#### 方案A:使用简化构建(推荐)
|
||||
```bash
|
||||
# 使用不需要terser的简化配置
|
||||
npm run build:simple
|
||||
```
|
||||
|
||||
#### 方案B:安装terser依赖
|
||||
```bash
|
||||
# 安装terser依赖
|
||||
npm run install:terser
|
||||
|
||||
# 然后正常构建
|
||||
npm run build:min
|
||||
```
|
||||
|
||||
#### 方案C:手动安装terser
|
||||
```bash
|
||||
npm install --save-dev terser
|
||||
npm run build:min
|
||||
```
|
||||
|
||||
### 2. 构建配置选择
|
||||
|
||||
我们提供了多个构建配置:
|
||||
|
||||
| 配置文件 | 用途 | 特点 |
|
||||
|----------|------|------|
|
||||
| `vite.lib.simple.config.js` | 简化构建 | 使用Vite内置压缩,无需额外依赖 |
|
||||
| `vite.lib.config.js` | 高级构建 | 使用terser,压缩效果更好 |
|
||||
|
||||
### 3. 构建输出文件
|
||||
|
||||
成功构建后,会在 `dist/` 目录下生成:
|
||||
- `za-player.min.js` - UMD格式,浏览器使用
|
||||
- `za-player.es.js` - ES模块格式,现代项目使用
|
||||
|
||||
### 4. 验证构建结果
|
||||
|
||||
构建完成后,可以通过以下方式验证:
|
||||
|
||||
#### 检查文件是否存在
|
||||
```bash
|
||||
ls -la dist/
|
||||
# Windows: dir dist\
|
||||
```
|
||||
|
||||
#### 检查文件大小
|
||||
```bash
|
||||
# 查看文件大小(应该有一定压缩)
|
||||
wc -c dist/za-player.min.js
|
||||
wc -c dist/za-player.es.js
|
||||
```
|
||||
|
||||
#### 测试功能
|
||||
打开 `test-build.html` 文件,在浏览器中测试两个构建文件是否正常工作。
|
||||
|
||||
### 5. 快速开始(推荐流程)
|
||||
|
||||
```bash
|
||||
# 1. 安装依赖
|
||||
npm install
|
||||
|
||||
# 2. 使用简化构建(避免terser问题)
|
||||
npm run build:simple
|
||||
|
||||
# 3. 验证构建结果
|
||||
ls dist/
|
||||
# 应该看到 za-player.min.js 和 za-player.es.js
|
||||
|
||||
# 4. 测试功能
|
||||
# 在浏览器中打开 test-build.html
|
||||
```
|
||||
|
||||
### 6. 如果仍然有问题
|
||||
|
||||
1. **清除node_modules并重新安装:**
|
||||
```bash
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **检查Node.js版本:**
|
||||
```bash
|
||||
node --version
|
||||
# 建议使用 Node.js 16+
|
||||
```
|
||||
|
||||
3. **检查Vite版本:**
|
||||
```bash
|
||||
npm list vite
|
||||
```
|
||||
|
||||
4. **查看详细错误信息:**
|
||||
```bash
|
||||
npm run build:simple -- --debug
|
||||
```
|
||||
|
||||
### 7. 获取帮助
|
||||
|
||||
如果以上方法都不能解决问题,请提供以下信息:
|
||||
1. 完整的错误信息
|
||||
2. Node.js版本 (`node --version`)
|
||||
3. npm版本 (`npm --version`)
|
||||
4. 操作系统信息
|
||||
5. 使用的构建命令
|
||||
177
README.md
177
README.md
@ -1,3 +1,176 @@
|
||||
# ZAPlayer
|
||||
# ZAPlayer 使用说明
|
||||
|
||||
ZAPlayer是一个支持HLS(M3U8)和FLV格式的视频播放器类库。
|
||||
ZAPlayer是一个支持HLS(M3U8)和FLV格式的视频播放器类库。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 方法1:直接引用构建好的类库文件
|
||||
|
||||
1. 首先构建类库文件:
|
||||
```bash
|
||||
npm run build:min
|
||||
```
|
||||
|
||||
2. 构建完成后,会在 `dist/` 目录下生成两个文件:
|
||||
- `za-player.min.js` - 压缩后的UMD格式,适合浏览器直接使用
|
||||
- `za-player.es.js` - ES模块格式,适合现代JavaScript项目
|
||||
|
||||
3. 在HTML文件中引用(浏览器使用):
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>ZAPlayer 示例</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 视频容器 -->
|
||||
<div id="videoContainer" style="width: 800px; height: 450px;"></div>
|
||||
|
||||
<!-- 引入ZAPlayer类库(浏览器版本)-->
|
||||
<script src="dist/za-player.min.js"></script>
|
||||
|
||||
<script>
|
||||
// 创建播放器实例
|
||||
var player = new ZAPlayer('#videoContainer', {
|
||||
type: 'flv', // 或 'hls'
|
||||
src: 'your-video-url.flv'
|
||||
});
|
||||
|
||||
// 控制播放器
|
||||
player.player.play();
|
||||
player.player.pause();
|
||||
player.player.stop();
|
||||
player.player.load('new-video-url.flv');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 方法2:使用ES模块
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>ZAPlayer ES模块示例</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="videoContainer" style="width: 800px; height: 450px;"></div>
|
||||
|
||||
<script type="module">
|
||||
import ZAPlayer from './dist/za-player.es.js';
|
||||
|
||||
const player = new ZAPlayer('#videoContainer', {
|
||||
type: 'hls',
|
||||
src: 'your-video-url.m3u8'
|
||||
});
|
||||
|
||||
// 控制播放器
|
||||
player.player.play();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 方法3:在Node.js或现代前端项目中使用ES模块
|
||||
|
||||
```javascript
|
||||
// 使用ES模块导入
|
||||
import ZAPlayer from './dist/za-player.es.js';
|
||||
|
||||
// 创建播放器
|
||||
const player = new ZAPlayer('#container', {
|
||||
type: 'flv',
|
||||
src: 'video.flv'
|
||||
});
|
||||
|
||||
player.player.play();
|
||||
```
|
||||
|
||||
### 方法4:动态加载(浏览器版本)
|
||||
|
||||
```javascript
|
||||
// 动态加载ZAPlayer类库
|
||||
function loadZAPlayer(callback) {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'dist/za-player.min.js'; // 使用压缩版本
|
||||
script.onload = callback;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
// 使用动态加载的类库
|
||||
loadZAPlayer(function() {
|
||||
const player = new ZAPlayer('#videoContainer', {
|
||||
type: 'flv',
|
||||
src: 'your-video-url.flv'
|
||||
});
|
||||
|
||||
player.player.play();
|
||||
});
|
||||
```
|
||||
|
||||
## API 说明
|
||||
|
||||
### 构造函数
|
||||
```javascript
|
||||
new ZAPlayer(container, options)
|
||||
```
|
||||
|
||||
**参数:**
|
||||
- `container` (string|Element): 视频容器的选择器字符串或DOM元素
|
||||
- `options` (object): 配置选项
|
||||
- `type` (string): 视频类型,'hls' 或 'flv',默认为 'hls'
|
||||
- `src` (string, 可选): 视频地址,可以在创建后通过load方法加载
|
||||
|
||||
**返回值:**
|
||||
- 返回一个对象,包含player属性,player对象有以下方法:
|
||||
- `play()`: 播放视频
|
||||
- `pause()`: 暂停视频
|
||||
- `stop()`: 停止视频
|
||||
- `load(url)`: 加载新的视频地址
|
||||
|
||||
## 示例视频地址
|
||||
|
||||
### FLV格式
|
||||
```javascript
|
||||
// 示例FLV地址
|
||||
player.player.load('http://192.168.1.200:10037/live/PUGE0hFBYluVe_01.flv');
|
||||
```
|
||||
|
||||
### HLS(M3U8)格式
|
||||
```javascript
|
||||
// 示例HLS地址
|
||||
player.player.load('http://192.168.1.201:9080/DS-2CD5026FWD20180811AACH220809006/0000000B/hls.m3u8');
|
||||
```
|
||||
|
||||
## 浏览器兼容性
|
||||
|
||||
- 支持现代浏览器的Media Source Extensions (MSE)
|
||||
- 支持WebCodecs API(用于高级解码)
|
||||
- 支持Fetch API
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保视频地址支持跨域访问(CORS)
|
||||
2. 对于直播流,确保服务器支持持续的数据传输
|
||||
3. 某些浏览器可能需要HTTPS协议才能正常工作
|
||||
|
||||
## 构建说明
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 开发模式
|
||||
npm run dev
|
||||
|
||||
# 构建演示页面
|
||||
npm run build
|
||||
|
||||
# 构建类库文件(同时生成两个文件)
|
||||
npm run build:min
|
||||
```
|
||||
|
||||
构建完成后,类库文件会在`dist/`目录下生成:
|
||||
- `za-player.min.js` - 压缩后的UMD格式类库文件,适合浏览器直接使用
|
||||
- `za-player.es.js` - ES模块格式,适合现代JavaScript项目和打包工具
|
||||
176
USAGE.md
Normal file
176
USAGE.md
Normal file
@ -0,0 +1,176 @@
|
||||
# ZAPlayer 使用说明
|
||||
|
||||
ZAPlayer是一个支持HLS(M3U8)和FLV格式的视频播放器类库。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 方法1:直接引用构建好的类库文件
|
||||
|
||||
1. 首先构建类库文件:
|
||||
```bash
|
||||
npm run build:min
|
||||
```
|
||||
|
||||
2. 构建完成后,会在 `dist/` 目录下生成两个文件:
|
||||
- `za-player.min.js` - 压缩后的UMD格式,适合浏览器直接使用
|
||||
- `za-player.es.js` - ES模块格式,适合现代JavaScript项目
|
||||
|
||||
3. 在HTML文件中引用(浏览器使用):
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>ZAPlayer 示例</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 视频容器 -->
|
||||
<div id="videoContainer" style="width: 800px; height: 450px;"></div>
|
||||
|
||||
<!-- 引入ZAPlayer类库(浏览器版本)-->
|
||||
<script src="dist/za-player.min.js"></script>
|
||||
|
||||
<script>
|
||||
// 创建播放器实例
|
||||
var player = new ZAPlayer('#videoContainer', {
|
||||
type: 'flv', // 或 'hls'
|
||||
src: 'your-video-url.flv'
|
||||
});
|
||||
|
||||
// 控制播放器
|
||||
player.player.play();
|
||||
player.player.pause();
|
||||
player.player.stop();
|
||||
player.player.load('new-video-url.flv');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 方法2:使用ES模块
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>ZAPlayer ES模块示例</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="videoContainer" style="width: 800px; height: 450px;"></div>
|
||||
|
||||
<script type="module">
|
||||
import ZAPlayer from './dist/za-player.es.js';
|
||||
|
||||
const player = new ZAPlayer('#videoContainer', {
|
||||
type: 'hls',
|
||||
src: 'your-video-url.m3u8'
|
||||
});
|
||||
|
||||
// 控制播放器
|
||||
player.player.play();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 方法3:在Node.js或现代前端项目中使用ES模块
|
||||
|
||||
```javascript
|
||||
// 使用ES模块导入
|
||||
import ZAPlayer from './dist/za-player.es.js';
|
||||
|
||||
// 创建播放器
|
||||
const player = new ZAPlayer('#container', {
|
||||
type: 'flv',
|
||||
src: 'video.flv'
|
||||
});
|
||||
|
||||
player.player.play();
|
||||
```
|
||||
|
||||
### 方法4:动态加载(浏览器版本)
|
||||
|
||||
```javascript
|
||||
// 动态加载ZAPlayer类库
|
||||
function loadZAPlayer(callback) {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'dist/za-player.min.js'; // 使用压缩版本
|
||||
script.onload = callback;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
// 使用动态加载的类库
|
||||
loadZAPlayer(function() {
|
||||
const player = new ZAPlayer('#videoContainer', {
|
||||
type: 'flv',
|
||||
src: 'your-video-url.flv'
|
||||
});
|
||||
|
||||
player.player.play();
|
||||
});
|
||||
```
|
||||
|
||||
## API 说明
|
||||
|
||||
### 构造函数
|
||||
```javascript
|
||||
new ZAPlayer(container, options)
|
||||
```
|
||||
|
||||
**参数:**
|
||||
- `container` (string|Element): 视频容器的选择器字符串或DOM元素
|
||||
- `options` (object): 配置选项
|
||||
- `type` (string): 视频类型,'hls' 或 'flv',默认为 'hls'
|
||||
- `src` (string, 可选): 视频地址,可以在创建后通过load方法加载
|
||||
|
||||
**返回值:**
|
||||
- 返回一个对象,包含player属性,player对象有以下方法:
|
||||
- `play()`: 播放视频
|
||||
- `pause()`: 暂停视频
|
||||
- `stop()`: 停止视频
|
||||
- `load(url)`: 加载新的视频地址
|
||||
|
||||
## 示例视频地址
|
||||
|
||||
### FLV格式
|
||||
```javascript
|
||||
// 示例FLV地址
|
||||
player.player.load('http://192.168.1.200:10037/live/PUGE0hFBYluVe_01.flv');
|
||||
```
|
||||
|
||||
### HLS(M3U8)格式
|
||||
```javascript
|
||||
// 示例HLS地址
|
||||
player.player.load('http://192.168.1.201:9080/DS-2CD5026FWD20180811AACH220809006/0000000B/hls.m3u8');
|
||||
```
|
||||
|
||||
## 浏览器兼容性
|
||||
|
||||
- 支持现代浏览器的Media Source Extensions (MSE)
|
||||
- 支持WebCodecs API(用于高级解码)
|
||||
- 支持Fetch API
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保视频地址支持跨域访问(CORS)
|
||||
2. 对于直播流,确保服务器支持持续的数据传输
|
||||
3. 某些浏览器可能需要HTTPS协议才能正常工作
|
||||
|
||||
## 构建说明
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 开发模式
|
||||
npm run dev
|
||||
|
||||
# 构建演示页面
|
||||
npm run build
|
||||
|
||||
# 构建类库文件(同时生成两个文件)
|
||||
npm run build:min
|
||||
```
|
||||
|
||||
构建完成后,类库文件会在`dist/`目录下生成:
|
||||
- `za-player.min.js` - 压缩后的UMD格式类库文件,适合浏览器直接使用
|
||||
- `za-player.es.js` - ES模块格式,适合现代JavaScript项目和打包工具
|
||||
66
index.html
Normal file
66
index.html
Normal file
@ -0,0 +1,66 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>qhlsplayer</title>
|
||||
<link rel="stylesheet" href="/src/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="player-container">
|
||||
<h1>ZAPlayer</h1>
|
||||
<div class="video-wrapper" style="width: 1280px; height: 720px;">
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input type="text" id="videoUrl" placeholder="输入视频地址" />
|
||||
<button id="loadBtn">加载</button>
|
||||
<button id="playBtn">播放</button>
|
||||
<button id="pauseBtn">暂停</button>
|
||||
<button id="stopBtn">停止</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="./src/main.js"></script>
|
||||
<script type="text/javascript">
|
||||
// 等待模块加载完成
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 绑定事件
|
||||
const videoUrl = document.getElementById('videoUrl');
|
||||
const loadBtn = document.getElementById('loadBtn');
|
||||
const playBtn = document.getElementById('playBtn');
|
||||
const pauseBtn = document.getElementById('pauseBtn');
|
||||
const stopBtn = document.getElementById('stopBtn');
|
||||
|
||||
var player = new ZAPlayer('.video-wrapper',{type:'flv'}).player;
|
||||
|
||||
loadBtn.addEventListener('click', () => {
|
||||
const url = videoUrl.value.trim();
|
||||
if (url) {
|
||||
player.load(url);
|
||||
} else {
|
||||
alert('请输入视频地址');
|
||||
}
|
||||
});
|
||||
|
||||
playBtn.addEventListener('click', () => {
|
||||
player.play();
|
||||
});
|
||||
|
||||
pauseBtn.addEventListener('click', () => {
|
||||
player.pause();
|
||||
});
|
||||
|
||||
stopBtn.addEventListener('click', () => {
|
||||
player.stop();
|
||||
});
|
||||
|
||||
// 示例M3U8地址(可替换为您自己的地址)
|
||||
//videoUrl.value = 'http://192.168.1.201:9080/DS-2CD5026FWD20180811AACH220809006/0000000B/hls.m3u8';
|
||||
videoUrl.value = 'http://192.168.1.200:10037/live/PUGE0hFBYluVe_01.flv?expired=20260323151552';
|
||||
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
26
install-deps.js
Normal file
26
install-deps.js
Normal file
@ -0,0 +1,26 @@
|
||||
// 依赖安装检查脚本
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
console.log('检查并安装必要的依赖...');
|
||||
|
||||
try {
|
||||
// 检查package.json是否存在
|
||||
if (!fs.existsSync('package.json')) {
|
||||
console.error('package.json 不存在!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 安装terser(如果需要高级压缩功能)
|
||||
console.log('安装 terser...');
|
||||
execSync('npm install --save-dev terser', { stdio: 'inherit' });
|
||||
|
||||
console.log('依赖安装完成!');
|
||||
console.log('可以运行以下命令进行构建:');
|
||||
console.log(' npm run build:min');
|
||||
|
||||
} catch (error) {
|
||||
console.error('依赖安装失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
1221
package-lock.json
generated
Normal file
1221
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
package.json
Normal file
22
package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "qhlsplayer",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build:lib": "vite build --config vite.lib.config.js",
|
||||
"build:simple": "vite build --config vite.lib.simple.config.js",
|
||||
"build:min": "npm run build:simple && echo '构建完成!文件输出为:dist/za-player.min.js 和 dist/za-player.es.js'",
|
||||
"install:terser": "npm install --save-dev terser",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"terser": "^5.46.0",
|
||||
"vite": "^7.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"flv.js": "^1.6.2"
|
||||
}
|
||||
}
|
||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
114
src/flv-player.js
Normal file
114
src/flv-player.js
Normal file
@ -0,0 +1,114 @@
|
||||
// 导入flv.js库
|
||||
import flvjs from 'flv.js';
|
||||
|
||||
class FLVPlayer {
|
||||
constructor(videoElement) {
|
||||
this.videoElement = videoElement;
|
||||
this.flvPlayer = null;
|
||||
this.flvUrl = null;
|
||||
this.isLoading = false;
|
||||
this.isPlaying = false;
|
||||
this.setupErrorHandlers();
|
||||
}
|
||||
|
||||
setupErrorHandlers() {
|
||||
// 监听视频元素错误
|
||||
this.videoElement.addEventListener('error', () => {
|
||||
console.error('视频元素错误:', this.videoElement.error);
|
||||
// 不要在错误事件中立即停止,这可能导致无限循环
|
||||
if (this.videoElement.error.code === 4 && this.videoElement.src === '') {
|
||||
// 忽略空src错误,这可能是正常的重置操作
|
||||
return;
|
||||
}
|
||||
this.stop();
|
||||
});
|
||||
}
|
||||
|
||||
async load(url) {
|
||||
try {
|
||||
// 重置播放器状态
|
||||
this.isLoading = true;
|
||||
this.flvUrl = url;
|
||||
|
||||
// 检查浏览器是否支持flv.js
|
||||
if (!flvjs.isSupported()) {
|
||||
throw new Error('浏览器不支持flv.js');
|
||||
}
|
||||
|
||||
// 如果已经有播放器实例,先销毁
|
||||
if (this.flvPlayer) {
|
||||
this.flvPlayer.destroy();
|
||||
}
|
||||
|
||||
// 创建新的flv.js播放器实例
|
||||
this.flvPlayer = flvjs.createPlayer({
|
||||
type: 'flv',
|
||||
url: url
|
||||
});
|
||||
|
||||
// 附加到video元素
|
||||
this.flvPlayer.attachMediaElement(this.videoElement);
|
||||
|
||||
// 监听flv.js错误事件
|
||||
this.flvPlayer.on(flvjs.Events.ERROR, (errorType, errorDetail, errorInfo) => {
|
||||
console.error('flv.js错误:', errorType, errorDetail, errorInfo);
|
||||
this.stop();
|
||||
});
|
||||
|
||||
// 加载视频
|
||||
await this.flvPlayer.load();
|
||||
|
||||
this.isPlaying = true;
|
||||
} catch (error) {
|
||||
console.error('加载FLV失败:', error);
|
||||
this.stop();
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加play方法
|
||||
play() {
|
||||
this.isPlaying = true;
|
||||
if (this.flvPlayer) {
|
||||
this.flvPlayer.play().catch(error => {
|
||||
console.error('播放失败:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 添加pause方法
|
||||
pause() {
|
||||
this.isPlaying = false;
|
||||
if (this.flvPlayer) {
|
||||
this.flvPlayer.pause();
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
// 停止播放
|
||||
this.isPlaying = false;
|
||||
this.isLoading = false;
|
||||
|
||||
// 暂停视频播放
|
||||
this.pause();
|
||||
|
||||
// 销毁flv.js播放器实例
|
||||
if (this.flvPlayer) {
|
||||
this.flvPlayer.destroy();
|
||||
this.flvPlayer = null;
|
||||
}
|
||||
|
||||
// 清除视频元素的src
|
||||
this.videoElement.src = '';
|
||||
this.videoElement.load();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// 完全销毁播放器
|
||||
this.stop();
|
||||
this.videoElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default FLVPlayer;
|
||||
670
src/m3u8-player.js
Normal file
670
src/m3u8-player.js
Normal file
@ -0,0 +1,670 @@
|
||||
class M3U8Player {
|
||||
constructor(videoElement) {
|
||||
this.videoElement = videoElement;
|
||||
this.mediaSource = null;
|
||||
this.sourceBuffer = null;
|
||||
this.segments = [];
|
||||
this.currentSegmentIndex = 0;
|
||||
this.isLoading = false;
|
||||
this.isLiveStream = false; // 标识是否为直播流
|
||||
this.refreshInterval = null; // 直播刷新定时器
|
||||
this.m3u8Url = null; // 保存M3U8地址
|
||||
this.setupErrorHandlers();
|
||||
|
||||
// WebCodecs相关属性
|
||||
this.useWebCodecs = false;
|
||||
this.videoDecoder = null;
|
||||
// this.audioDecoder = null;
|
||||
this.videoQueue = [];
|
||||
this.audioQueue = [];
|
||||
this.isPlaying = false;
|
||||
this.startTime = 0;
|
||||
this.canvas = null;
|
||||
this.ctx = null;
|
||||
this.audioContext = null;
|
||||
this.audioSource = null;
|
||||
this.audioProcessor = null;
|
||||
this.frameRate = 30;
|
||||
this.lastRenderTime = 0;
|
||||
}
|
||||
|
||||
setupErrorHandlers() {
|
||||
// 监听视频元素错误
|
||||
this.videoElement.addEventListener('error', () => {
|
||||
console.error('视频元素错误:', this.videoElement.error);
|
||||
// 发生错误时跳过当前片段
|
||||
this.currentSegmentIndex++;
|
||||
setTimeout(() => this.loadNextSegment(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
async load(url) {
|
||||
try {
|
||||
// 完全重置播放器状态
|
||||
this.stop();
|
||||
|
||||
// 保存M3U8地址
|
||||
this.m3u8Url = url;
|
||||
|
||||
// 完全清除视频元素的src和缓冲
|
||||
this.videoElement.src = '';
|
||||
this.videoElement.load();
|
||||
|
||||
// 初始化WebCodecs模式的canvas
|
||||
this.initWebCodecsCanvas();
|
||||
|
||||
// 创建新的MediaSource
|
||||
this.mediaSource = new MediaSource();
|
||||
this.videoElement.src = URL.createObjectURL(this.mediaSource);
|
||||
|
||||
await this.waitForMediaSourceOpen();
|
||||
await this.parseM3U8(url);
|
||||
this.startLoading();
|
||||
|
||||
// 如果是直播流,设置定期刷新
|
||||
if (this.isLiveStream) {
|
||||
this.setupLiveRefresh();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载M3U8失败:', error);
|
||||
this.stop();
|
||||
}
|
||||
}
|
||||
|
||||
initWebCodecsCanvas() {
|
||||
// 创建canvas元素用于WebCodecs渲染
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas.style.display = 'none';
|
||||
this.canvas.width = this.videoElement.clientWidth || 640;
|
||||
this.canvas.height = this.videoElement.clientHeight || 360;
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
|
||||
// 将canvas插入到video元素后面
|
||||
this.videoElement.parentNode.insertBefore(this.canvas, this.videoElement.nextSibling);
|
||||
}
|
||||
|
||||
waitForMediaSourceOpen() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.mediaSource && this.mediaSource.readyState === 'open') {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('MediaSource打开超时'));
|
||||
}, 5000);
|
||||
|
||||
const onSourceOpen = () => {
|
||||
clearTimeout(timeout);
|
||||
this.mediaSource.removeEventListener('sourceopen', onSourceOpen);
|
||||
this.mediaSource.removeEventListener('sourceerror', onSourceError);
|
||||
resolve();
|
||||
};
|
||||
|
||||
const onSourceError = (e) => {
|
||||
clearTimeout(timeout);
|
||||
this.mediaSource.removeEventListener('sourceopen', onSourceOpen);
|
||||
this.mediaSource.removeEventListener('sourceerror', onSourceError);
|
||||
reject(new Error('MediaSource打开错误'));
|
||||
};
|
||||
|
||||
this.mediaSource.addEventListener('sourceopen', onSourceOpen);
|
||||
this.mediaSource.addEventListener('sourceerror', onSourceError);
|
||||
});
|
||||
}
|
||||
|
||||
async parseM3U8(url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const text = await response.text();
|
||||
const lines = text.split('\n');
|
||||
let duration = 0;
|
||||
let segmentUrl = '';
|
||||
let newSegments = [];
|
||||
let currentTimestamp = 0;
|
||||
|
||||
// 检查是否为直播流(没有#EXT-X-ENDLIST标签)
|
||||
this.isLiveStream = !text.includes('#EXT-X-ENDLIST');
|
||||
|
||||
// 计算每个片段的时间戳范围
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (line.startsWith('#EXTINF:')) {
|
||||
// 获取片段时长
|
||||
duration = parseFloat(line.split(':')[1].split(',')[0]);
|
||||
} else if (!line.startsWith('#') && line) {
|
||||
// 构建完整的片段URL
|
||||
if (line.startsWith('http')) {
|
||||
segmentUrl = line;
|
||||
} else {
|
||||
const baseUrl = url.substring(0, url.lastIndexOf('/') + 1);
|
||||
segmentUrl = baseUrl + line;
|
||||
}
|
||||
|
||||
// 添加时间戳信息
|
||||
newSegments.push({
|
||||
url: segmentUrl,
|
||||
duration,
|
||||
startTime: currentTimestamp,
|
||||
endTime: currentTimestamp + duration
|
||||
});
|
||||
|
||||
currentTimestamp += duration;
|
||||
}
|
||||
}
|
||||
|
||||
//console.log('解析到的片段:', newSegments);
|
||||
|
||||
// 如果是直播流且不是第一次加载,需要特殊处理
|
||||
if (this.isLiveStream && this.segments.length > 0) {
|
||||
// 找出真正的新片段(基于URL比较)
|
||||
const existingUrls = new Set(this.segments.map(s => s.url));
|
||||
const newAddedSegments = newSegments.filter(segment => !existingUrls.has(segment.url));
|
||||
|
||||
// 确保新片段的时间戳是递增的
|
||||
if (newAddedSegments.length > 0) {
|
||||
// 找到最后一个现有片段的结束时间
|
||||
const lastEndTime = this.segments[this.segments.length - 1].endTime;
|
||||
|
||||
// 调整新片段的时间戳
|
||||
let adjustedTimestamp = lastEndTime;
|
||||
newAddedSegments.forEach(segment => {
|
||||
segment.startTime = adjustedTimestamp;
|
||||
segment.endTime = adjustedTimestamp + segment.duration;
|
||||
adjustedTimestamp += segment.duration;
|
||||
});
|
||||
|
||||
// 添加新片段到现有列表
|
||||
this.segments = [...this.segments, ...newAddedSegments];
|
||||
|
||||
//console.log('新增片段:', newAddedSegments);
|
||||
}
|
||||
} else {
|
||||
// 第一次加载或非直播流,直接使用新列表
|
||||
this.segments = newSegments;
|
||||
this.currentSegmentIndex = 0; // 确保从第一个片段开始播放
|
||||
}
|
||||
|
||||
return this.segments;
|
||||
} catch (error) {
|
||||
console.error('解析M3U8失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
startLoading() {
|
||||
this.isLoading = true;
|
||||
this.loadNextSegment();
|
||||
}
|
||||
|
||||
async loadNextSegment() {
|
||||
if (!this.isLoading || !this.segments.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 对于直播流,确保索引不越界
|
||||
if (this.isLiveStream && this.currentSegmentIndex >= this.segments.length) {
|
||||
// 等待新的片段
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
this.loadNextSegment();
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保索引在有效范围内
|
||||
if (this.currentSegmentIndex >= this.segments.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const segment = this.segments[this.currentSegmentIndex];
|
||||
const response = await fetch(segment.url);
|
||||
const data = await response.arrayBuffer();
|
||||
|
||||
if (!this.sourceBuffer && !this.useWebCodecs) {
|
||||
// 提供更全面的MIME类型和编解码器组合的后备方案
|
||||
const mimeTypes = [
|
||||
// 首选:具体的编解码器组合
|
||||
'video/mp2t; codecs="avc1.42E01E, mp4a.40.2"', // 完整编解码器(原方案)
|
||||
'video/mp2t; codecs="avc1.42001E, mp4a.40.2"', // 另一种常见的编解码器组合
|
||||
'video/mp2t; codecs="avc1.64001F, mp4a.40.2"', // 更高质量的视频编解码器
|
||||
'video/mp2t; codecs="avc1.4D401E, mp4a.40.2"', // 另一种常见的H.264编解码器
|
||||
|
||||
// 备选:只指定视频编解码器
|
||||
'video/mp2t; codecs="avc1.42E01E"',
|
||||
'video/mp2t; codecs="avc1.42001E"',
|
||||
'video/mp2t; codecs="h264"',
|
||||
|
||||
// 备选:只指定音频编解码器
|
||||
'video/mp2t; codecs="mp4a.40.2"',
|
||||
'video/mp2t; codecs="aac"',
|
||||
|
||||
// 最后选择:不指定编解码器或使用其他容器格式
|
||||
'video/mp2t',
|
||||
'video/x-mpeg2-transport-stream',
|
||||
|
||||
'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'
|
||||
];
|
||||
|
||||
// 尝试找到浏览器支持的MIME类型
|
||||
let supportedMimeType = null;
|
||||
for (const mimeType of mimeTypes) {
|
||||
if (MediaSource.isTypeSupported(mimeType)) {
|
||||
supportedMimeType = mimeType;
|
||||
console.log('使用支持的MIME类型:', mimeType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!supportedMimeType) {
|
||||
// 尝试使用最基础的类型,如果失败则会在下面的try-catch中处理
|
||||
supportedMimeType = 'video/mp2t';
|
||||
}
|
||||
|
||||
// 在调用addSourceBuffer前再次检查
|
||||
if (MediaSource.isTypeSupported(supportedMimeType)) {
|
||||
try {
|
||||
// 使用sequence模式更适合直接播放
|
||||
this.sourceBuffer = this.mediaSource.addSourceBuffer(supportedMimeType);
|
||||
this.sourceBuffer.mode = 'sequence';
|
||||
this.setupSourceBuffer();
|
||||
} catch (error) {
|
||||
console.error('创建SourceBuffer失败:', error);
|
||||
// 尝试其他方法,比如使用更简单的格式
|
||||
try {
|
||||
this.sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t; codecs="h264,aac"');
|
||||
this.sourceBuffer.mode = 'sequence';
|
||||
this.setupSourceBuffer();
|
||||
} catch (e) {
|
||||
console.error('再次创建SourceBuffer失败:', e);
|
||||
// 如果都失败,尝试使用WebCodecs
|
||||
console.log('尝试使用WebCodecs解码视频');
|
||||
this.useWebCodecs = true;
|
||||
await this.initWebCodecs();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 使用WebCodecs解码视频
|
||||
console.log('MIME类型不受支持,尝试使用WebCodecs解码视频');
|
||||
this.useWebCodecs = true;
|
||||
await this.initWebCodecs();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.useWebCodecs) {
|
||||
// 使用WebCodecs解码
|
||||
await this.decodeWithWebCodecs(data, segment);
|
||||
} else {
|
||||
// 使用MediaSource解码
|
||||
await this.appendBuffer(data, segment);
|
||||
}
|
||||
|
||||
this.currentSegmentIndex++;
|
||||
this.loadNextSegment();
|
||||
} catch (error) {
|
||||
console.error('加载片段失败:', error);
|
||||
// 跳过当前片段,继续加载下一个
|
||||
this.currentSegmentIndex++;
|
||||
setTimeout(() => this.loadNextSegment(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
async initWebCodecs() {
|
||||
// 检查浏览器是否支持WebCodecs
|
||||
if (!('VideoDecoder' in window) || !('AudioDecoder' in window)) {
|
||||
throw new Error('浏览器不支持WebCodecs API');
|
||||
}
|
||||
|
||||
// 初始化视频解码器
|
||||
this.videoDecoder = new VideoDecoder({
|
||||
output: this.decodeVideoChunk.bind(this),
|
||||
error: (e) => {
|
||||
console.error('视频解码错误:', e);
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化音频解码器
|
||||
// this.audioDecoder = new AudioDecoder({
|
||||
// output: this.decodeAudioChunk.bind(this),
|
||||
// error: (e) => {
|
||||
// console.error('音频解码错误:', e);
|
||||
// }
|
||||
// });
|
||||
|
||||
// 配置解码器(取消注释并确保配置完成)
|
||||
try {
|
||||
await this.videoDecoder.configure({
|
||||
codec: 'avc1.42E01E',
|
||||
hardwareAcceleration: 'prefer-hardware'
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('视频解码器配置失败:', e);
|
||||
// 尝试使用其他编解码器
|
||||
await this.videoDecoder.configure({
|
||||
codec: 'h264',
|
||||
hardwareAcceleration: 'prefer-hardware'
|
||||
});
|
||||
}
|
||||
|
||||
// try {
|
||||
// await this.audioDecoder.configure({
|
||||
// codec: 'mp4a.40.2'
|
||||
// });
|
||||
// } catch (e) {
|
||||
// console.error('音频解码器配置失败:', e);
|
||||
// // 尝试使用其他编解码器
|
||||
// try {
|
||||
// await this.audioDecoder.configure({
|
||||
// codec: 'aac'
|
||||
// });
|
||||
// } catch (e2) {
|
||||
// console.error('音频解码器再次配置失败:', e2);
|
||||
// // 如果音频配置失败,我们仍然可以继续,只播放视频
|
||||
// }
|
||||
// }
|
||||
|
||||
// 显示canvas,隐藏video元素
|
||||
this.canvas.style.display = 'block';
|
||||
this.videoElement.style.display = 'none';
|
||||
|
||||
// 开始渲染循环
|
||||
this.isPlaying = true;
|
||||
this.startTime = performance.now();
|
||||
this.renderLoop();
|
||||
}
|
||||
|
||||
decodeVideoChunk(frame) {
|
||||
this.videoQueue.push({
|
||||
frame,
|
||||
timestamp: frame.timestamp / 1000000 // 转换为毫秒
|
||||
});
|
||||
}
|
||||
|
||||
decodeAudioChunk(frame) {
|
||||
this.audioQueue.push({
|
||||
frame,
|
||||
timestamp: frame.timestamp / 1000000 // 转换为毫秒
|
||||
});
|
||||
}
|
||||
|
||||
async decodeWithWebCodecs(data, segment) {
|
||||
// 检查解码器是否已配置
|
||||
if (!this.videoDecoder || this.videoDecoder.state !== 'configured') {
|
||||
console.error('视频解码器未配置');
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
renderLoop() {
|
||||
if (!this.isPlaying) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = performance.now() - this.startTime;
|
||||
|
||||
// 渲染视频帧
|
||||
while (this.videoQueue.length > 0 && this.videoQueue[0].timestamp <= currentTime) {
|
||||
const { frame } = this.videoQueue.shift();
|
||||
this.renderFrame(frame);
|
||||
frame.close();
|
||||
}
|
||||
|
||||
// 播放音频帧
|
||||
while (this.audioQueue.length > 0 && this.audioQueue[0].timestamp <= currentTime) {
|
||||
const { frame } = this.audioQueue.shift();
|
||||
this.playAudioFrame(frame);
|
||||
frame.close();
|
||||
}
|
||||
|
||||
// 继续渲染循环
|
||||
requestAnimationFrame(() => this.renderLoop());
|
||||
}
|
||||
|
||||
renderFrame(frame) {
|
||||
if (!this.ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 调整canvas大小
|
||||
if (this.canvas.width !== frame.displayWidth || this.canvas.height !== frame.displayHeight) {
|
||||
this.canvas.width = frame.displayWidth;
|
||||
this.canvas.height = frame.displayHeight;
|
||||
}
|
||||
|
||||
// 渲染帧
|
||||
this.ctx.drawImage(frame, 0, 0);
|
||||
}
|
||||
|
||||
playAudioFrame(frame) {
|
||||
// 初始化音频上下文
|
||||
if (!this.audioContext) {
|
||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
}
|
||||
|
||||
// 创建音频缓冲区
|
||||
const buffer = this.audioContext.createBuffer(
|
||||
frame.numberOfChannels,
|
||||
frame.length,
|
||||
frame.sampleRate
|
||||
);
|
||||
|
||||
// 填充音频数据
|
||||
for (let channel = 0; channel < frame.numberOfChannels; channel++) {
|
||||
const data = buffer.getChannelData(channel);
|
||||
frame.copyTo(data, { planeIndex: channel });
|
||||
}
|
||||
|
||||
// 播放音频
|
||||
const source = this.audioContext.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
source.connect(this.audioContext.destination);
|
||||
source.start();
|
||||
}
|
||||
|
||||
setupSourceBuffer() {
|
||||
this.sourceBuffer.addEventListener('updateend', () => {
|
||||
// 检查视频元素和MediaSource的状态
|
||||
if (this.videoElement.error) {
|
||||
console.error('视频元素出错,停止更新:', this.videoElement.error);
|
||||
this.currentSegmentIndex++;
|
||||
setTimeout(() => this.loadNextSegment(), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查MediaSource状态
|
||||
if (!this.mediaSource || this.mediaSource.readyState !== 'open') {
|
||||
console.error('MediaSource未打开,停止SourceBuffer更新');
|
||||
return;
|
||||
}
|
||||
|
||||
// 立即播放,不等待缓冲
|
||||
if (!this.videoElement.paused && this.videoElement.readyState > 2) {
|
||||
this.videoElement.play().catch(error => {
|
||||
console.error('自动播放失败:', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 简化的appendBuffer方法,不做缓冲处理
|
||||
appendBuffer(data, segment) {
|
||||
return new Promise((resolve) => {
|
||||
// 检查数据是否为空
|
||||
if (!data || data.byteLength === 0) {
|
||||
console.error('数据为空,无法追加缓冲区');
|
||||
// 数据为空,跳过当前片段
|
||||
this.currentSegmentIndex++;
|
||||
setTimeout(() => this.loadNextSegment(), 0);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查视频元素是否已卸载
|
||||
if (!this.videoElement || this.videoElement.disconnected) {
|
||||
console.error('视频元素已卸载,无法追加缓冲区');
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查当前片段是否有效
|
||||
if (!segment || !segment.url) {
|
||||
console.error('无效的片段信息');
|
||||
// 无效片段,跳过当前片段
|
||||
this.currentSegmentIndex++;
|
||||
setTimeout(() => this.loadNextSegment(), 0);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const append = () => {
|
||||
// 检查视频元素错误
|
||||
if (this.videoElement.error) {
|
||||
console.error('视频元素错误,无法追加缓冲区:', this.videoElement.error);
|
||||
this.currentSegmentIndex++;
|
||||
setTimeout(() => this.loadNextSegment(), 0);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.mediaSource) {
|
||||
console.error('MediaSource未初始化,无法追加缓冲区');
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.mediaSource.readyState !== 'open') {
|
||||
console.warn('MediaSource未打开,无法追加缓冲区');
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.sourceBuffer || this.sourceBuffer.updating) {
|
||||
setTimeout(append, 10);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.sourceBuffer.appendBuffer(data);
|
||||
//console.log('追加片段:', segment.url);
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error('追加Buffer失败:', error);
|
||||
// 任何错误都跳过当前片段
|
||||
this.currentSegmentIndex++;
|
||||
setTimeout(() => this.loadNextSegment(), 0);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
append();
|
||||
});
|
||||
}
|
||||
|
||||
setupLiveRefresh() {
|
||||
// 每隔几秒重新请求M3U8文件
|
||||
this.refreshInterval = setInterval(async () => {
|
||||
if (!this.isLiveStream || !this.isLoading) return;
|
||||
|
||||
try {
|
||||
await this.parseM3U8(this.m3u8Url);
|
||||
} catch (error) {
|
||||
console.error('刷新M3U8失败:', error);
|
||||
}
|
||||
}, 3000); // 每3秒刷新一次
|
||||
}
|
||||
|
||||
play() {
|
||||
if (this.useWebCodecs) {
|
||||
// WebCodecs模式
|
||||
this.isPlaying = true;
|
||||
this.startTime = performance.now() - (this.lastRenderTime || 0);
|
||||
this.renderLoop();
|
||||
} else {
|
||||
// MediaSource模式
|
||||
// 检查src是否为空
|
||||
if (!this.videoElement.src) {
|
||||
console.error('视频源地址为空,请先加载M3U8文件');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.videoElement.error) {
|
||||
console.error('视频元素出错,无法播放:', this.videoElement.error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.videoElement.play().catch(error => {
|
||||
console.error('播放失败:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (this.useWebCodecs) {
|
||||
// WebCodecs模式
|
||||
this.isPlaying = false;
|
||||
} else {
|
||||
// MediaSource模式
|
||||
this.videoElement.pause();
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.isLoading = false;
|
||||
this.isPlaying = false;
|
||||
this.currentSegmentIndex = 0;
|
||||
this.lastRenderTime = 0;
|
||||
|
||||
// 清除直播刷新定时器
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
this.refreshInterval = null;
|
||||
}
|
||||
|
||||
if (this.mediaSource) {
|
||||
try {
|
||||
URL.revokeObjectURL(this.videoElement.src);
|
||||
} catch (e) {
|
||||
console.error('撤销ObjectURL失败:', e);
|
||||
}
|
||||
this.mediaSource = null;
|
||||
this.sourceBuffer = null;
|
||||
}
|
||||
|
||||
// 释放WebCodecs资源
|
||||
if (this.videoDecoder) {
|
||||
this.videoDecoder.close();
|
||||
this.videoDecoder = null;
|
||||
}
|
||||
|
||||
// 注意:当前版本未使用音频解码器
|
||||
// if (this.audioDecoder) {
|
||||
// this.audioDecoder.close();
|
||||
// this.audioDecoder = null;
|
||||
// }
|
||||
|
||||
if (this.audioContext) {
|
||||
this.audioContext.close();
|
||||
this.audioContext = null;
|
||||
}
|
||||
|
||||
// 清空队列
|
||||
this.videoQueue = [];
|
||||
this.audioQueue = [];
|
||||
|
||||
try {
|
||||
this.videoElement.pause();
|
||||
this.videoElement.src = '';
|
||||
this.videoElement.load();
|
||||
|
||||
// 恢复显示video元素,隐藏canvas
|
||||
this.videoElement.style.display = 'block';
|
||||
if (this.canvas) {
|
||||
this.canvas.style.display = 'none';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('重置视频元素失败:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default M3U8Player;
|
||||
29
src/main.js
Normal file
29
src/main.js
Normal file
@ -0,0 +1,29 @@
|
||||
import M3U8Player from './m3u8-player.js'
|
||||
import FLVPlayer from './flv-player.js'
|
||||
|
||||
class ZAPlayer {
|
||||
constructor(videoWrapper,params={type:'hls'}){
|
||||
videoWrapper = (typeof videoWrapper === 'string') ? document.querySelector(videoWrapper)||document.getElementById(videoWrapper) : videoWrapper;
|
||||
videoWrapper.innerHTML = '<video id="videoPlayer" controls autoplay style="width: 100%; height: 100%;"></video>';
|
||||
const videoElement = document.getElementById('videoPlayer');
|
||||
|
||||
if(params.type==='hls'){
|
||||
// 初始化hls播放器
|
||||
this.player = new M3U8Player(videoElement);
|
||||
if(params.src){
|
||||
this.player.load(params.src);
|
||||
}
|
||||
}else{
|
||||
//flv视频播放支持
|
||||
this.player = new FLVPlayer(videoElement);
|
||||
if(params.src){
|
||||
this.player.load(params.src);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将ZAPlayer挂载到window对象,使其成为全局可用的类库
|
||||
window.ZAPlayer = ZAPlayer;
|
||||
|
||||
export default ZAPlayer;
|
||||
107
src/style.css
Normal file
107
src/style.css
Normal file
@ -0,0 +1,107 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#app {
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.player-container {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.video-wrapper {
|
||||
margin: 20px 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
background: #000;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
padding: 0.6em 1.2em;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 1em;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
input[type="text"] {
|
||||
background-color: #ffffff;
|
||||
color: #213547;
|
||||
border-color: #ccc;
|
||||
}
|
||||
}
|
||||
92
test-build.html
Normal file
92
test-build.html
Normal file
@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ZAPlayer 构建测试</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.test-section { margin: 30px 0; padding: 20px; border: 1px solid #ccc; }
|
||||
.video-container { width: 640px; height: 360px; margin: 10px 0; }
|
||||
.success { color: green; }
|
||||
.error { color: red; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>ZAPlayer 构建文件测试</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>测试1:UMD格式 (za-player.min.js)</h2>
|
||||
<div id="container1" class="video-container"></div>
|
||||
<div id="status1"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>测试2:ES模块格式 (za-player.es.js)</h2>
|
||||
<div id="container2" class="video-container"></div>
|
||||
<div id="status2"></div>
|
||||
</div>
|
||||
|
||||
<!-- 测试UMD格式 -->
|
||||
<script src="dist/za-player.min.js"></script>
|
||||
<script>
|
||||
function testUMD() {
|
||||
const status = document.getElementById('status1');
|
||||
try {
|
||||
if (typeof ZAPlayer === 'undefined') {
|
||||
throw new Error('ZAPlayer 未定义 - UMD构建可能失败');
|
||||
}
|
||||
|
||||
const player = new ZAPlayer('#container1', {
|
||||
type: 'flv',
|
||||
src: 'http://192.168.1.200:10037/live/PUGE0hFBYluVe_01.flv?expired=20260323151552'
|
||||
});
|
||||
|
||||
if (player && player.player) {
|
||||
status.innerHTML = '<span class="success">✓ UMD格式测试成功!ZAPlayer可用</span>';
|
||||
console.log('UMD格式测试通过');
|
||||
} else {
|
||||
throw new Error('播放器创建失败');
|
||||
}
|
||||
} catch (error) {
|
||||
status.innerHTML = `<span class="error">✗ UMD格式测试失败: ${error.message}</span>`;
|
||||
console.error('UMD格式测试失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 延迟执行,确保脚本加载完成
|
||||
setTimeout(testUMD, 100);
|
||||
</script>
|
||||
|
||||
<!-- 测试ES模块格式 -->
|
||||
<script type="module">
|
||||
import ZAPlayer from './dist/za-player.es.js';
|
||||
|
||||
function testES() {
|
||||
const status = document.getElementById('status2');
|
||||
try {
|
||||
if (typeof ZAPlayer === 'undefined') {
|
||||
throw new Error('ZAPlayer 未定义 - ES模块构建可能失败');
|
||||
}
|
||||
|
||||
const player = new ZAPlayer('#container2', {
|
||||
type: 'hls',
|
||||
src: 'http://192.168.1.201:9080/DS-2CD5026FWD20180811AACH220809006/0000000B/hls.m3u8'
|
||||
});
|
||||
|
||||
if (player && player.player) {
|
||||
status.innerHTML = '<span class="success">✓ ES模块格式测试成功!ZAPlayer可用</span>';
|
||||
console.log('ES模块格式测试通过');
|
||||
} else {
|
||||
throw new Error('播放器创建失败');
|
||||
}
|
||||
} catch (error) {
|
||||
status.innerHTML = `<span class="error">✗ ES模块格式测试失败: ${error.message}</span>`;
|
||||
console.error('ES模块格式测试失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
testES();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
15
vite.config.js
Normal file
15
vite.config.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: 'index.html'
|
||||
}
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true
|
||||
}
|
||||
})
|
||||
31
vite.lib.config.js
Normal file
31
vite.lib.config.js
Normal file
@ -0,0 +1,31 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/main.js'),
|
||||
name: 'ZAPlayer',
|
||||
fileName: (format) => {
|
||||
// 根据格式生成不同的文件名
|
||||
if (format === 'es') {
|
||||
return 'za-player.es.js' // ES模块格式
|
||||
} else if (format === 'umd') {
|
||||
return 'za-player.min.js' // UMD压缩格式
|
||||
}
|
||||
return `za-player.${format}.js`
|
||||
},
|
||||
formats: ['es', 'umd'] // 同时生成ES和UMD格式
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [],
|
||||
output: {
|
||||
globals: {}
|
||||
}
|
||||
},
|
||||
// 使用Vite内置的压缩(esbuild),不需要额外依赖
|
||||
minify: 'esbuild', // 或者使用 true(Vite默认压缩)
|
||||
// 不生成source map
|
||||
sourcemap: false
|
||||
}
|
||||
})
|
||||
29
vite.lib.simple.config.js
Normal file
29
vite.lib.simple.config.js
Normal file
@ -0,0 +1,29 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/main.js'),
|
||||
name: 'ZAPlayer',
|
||||
fileName: (format) => {
|
||||
if (format === 'es') {
|
||||
return 'za-player.es.js'
|
||||
} else if (format === 'umd') {
|
||||
return 'za-player.min.js'
|
||||
}
|
||||
return `za-player.${format}.js`
|
||||
},
|
||||
formats: ['es', 'umd']
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [],
|
||||
output: {
|
||||
globals: {}
|
||||
}
|
||||
},
|
||||
// 使用esbuild压缩(Vite内置,无需额外依赖)
|
||||
minify: true,
|
||||
sourcemap: false
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user