初始提交

This commit is contained in:
陈乾 2026-01-30 11:24:39 +08:00
parent 0562dd9885
commit 53c5bdd28e
17 changed files with 2917 additions and 2 deletions

25
.gitignore vendored Normal file
View 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
View 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
View File

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

File diff suppressed because it is too large Load Diff

22
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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>测试1UMD格式 (za-player.min.js)</h2>
<div id="container1" class="video-container"></div>
<div id="status1"></div>
</div>
<div class="test-section">
<h2>测试2ES模块格式 (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
View 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
View 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', // 或者使用 trueVite默认压缩
// 不生成source map
sourcemap: false
}
})

29
vite.lib.simple.config.js Normal file
View 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
}
})