mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-12 15:26:16 -08:00
Enhance property handling in artplayerProxyMediabunny
- Updated the way properties from the shim instance and its prototype are retrieved. - Replaced the for-in loop with a Set to ensure all properties, including inherited ones, are considered. - Excluded the 'constructor' property from being added to the canvas. refactor: remove unused utility functions and streamline event handling - Deleted `videoEvents.js`, `clamp.js`, and `sleep.js` as they were not utilized. - Introduced a new `EventTarget` class to manage event listeners more efficiently. - Enhanced `MediaBunnyEngine`, `AudioEngine`, `VideoEngine`, and `VideoShim` to utilize the new event system. - Updated type definitions in `artplayer-proxy-mediabunny.d.ts` to include new configuration options. - Added comprehensive architecture documentation to clarify the structure and flow of the MediaBunny engine. - Refactored the codebase for improved readability and maintainability, including better naming conventions and JSDoc comments.
This commit is contained in:
1 parent
59e9a32ecb
commit
be44c1f8dd
25 files changed
+2299
-10309
No files matched your search
@@ -3,7 +3,7 @@
|
||||
|
||||
const art = new Artplayer({
|
||||
container: '.artplayer-app',
|
||||
url: '/assets/sample/frag_bunny.mp4',
|
||||
url: 'https://artplayer.org/assets/sample/frag_bunny.mp4',
|
||||
autoSize: false,
|
||||
screenshot: true,
|
||||
setting: true,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1 +1 @@
|
||||
{"mappings":"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 truncated
|
||||
{"mappings":"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 truncated
|
||||
@@ -0,0 +1,167 @@
|
||||
# Architecture Documentation
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.js # 主入口,创建canvas代理
|
||||
├── VideoShim.js # HTMLVideoElement接口模拟层
|
||||
├── MediaBunnyEngine.js # 主引擎,协调音视频播放
|
||||
├── AudioEngine.js # 音频引擎,处理音频播放
|
||||
├── VideoEngine.js # 视频引擎,处理视频帧渲染
|
||||
└── EventTarget.js # 事件系统实现
|
||||
```
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 层次结构
|
||||
|
||||
```
|
||||
ArtPlayer
|
||||
↓
|
||||
index.js (Canvas Proxy)
|
||||
↓
|
||||
VideoShim (HTMLVideoElement Interface)
|
||||
↓
|
||||
MediaBunnyEngine (Coordination Layer)
|
||||
├→ AudioEngine (Web Audio API)
|
||||
└→ VideoEngine (Canvas Rendering)
|
||||
```
|
||||
|
||||
### 核心组件
|
||||
|
||||
#### 1. index.js
|
||||
- **职责**: 插件入口点,创建canvas元素并代理到VideoShim
|
||||
- **功能**:
|
||||
- 创建canvas和2D上下文
|
||||
- 实例化VideoShim
|
||||
- 将VideoShim的属性和方法代理到canvas对象
|
||||
- 处理ArtPlayer的resize事件
|
||||
- 管理生命周期(destroy)
|
||||
|
||||
#### 2. VideoShim.js
|
||||
- **职责**: 模拟HTMLVideoElement接口
|
||||
- **功能**:
|
||||
- 实现标准video元素的所有属性(currentTime, duration, paused等)
|
||||
- 实现播放控制方法(play, pause, seek)
|
||||
- 处理音量和播放速率
|
||||
- 转发事件到ArtPlayer
|
||||
- 管理MediaBunnyEngine实例
|
||||
|
||||
#### 3. MediaBunnyEngine.js
|
||||
- **职责**: 协调音频和视频引擎
|
||||
- **功能**:
|
||||
- 管理加载流程(load timeout, metadata loading)
|
||||
- 协调play/pause/seek操作
|
||||
- 同步readyState和networkState
|
||||
- 错误处理
|
||||
- 管理播放状态(paused, ended, seeking)
|
||||
|
||||
#### 4. AudioEngine.js
|
||||
- **职责**: 音频播放和时钟源
|
||||
- **功能**:
|
||||
- 使用MediaBunny加载音频轨道
|
||||
- Web Audio API播放音频
|
||||
- 提供准确的currentTime(作为主时钟)
|
||||
- 音量控制和静音
|
||||
- 播放速率调整
|
||||
- 缓冲区监控(starvation detection)
|
||||
|
||||
#### 5. VideoEngine.js
|
||||
- **职责**: 视频帧渲染和同步
|
||||
- **功能**:
|
||||
- 使用MediaBunny解码视频帧
|
||||
- 将帧渲染到canvas
|
||||
- 与音频时钟同步
|
||||
- 处理late frames(可选丢帧)
|
||||
- Poster图片显示
|
||||
- Preflight检查(range requests支持)
|
||||
|
||||
#### 6. EventTarget.js
|
||||
- **职责**: 简单的事件系统
|
||||
- **功能**:
|
||||
- addEventListener
|
||||
- removeEventListener
|
||||
- emit事件
|
||||
|
||||
## 数据流
|
||||
|
||||
### 加载流程
|
||||
```
|
||||
MediaBunnyEngine.load()
|
||||
├→ AudioEngine.load()
|
||||
│ ├→ 创建Input
|
||||
│ ├→ 获取音频轨道
|
||||
│ ├→ 创建AudioBufferSink
|
||||
│ └→ 触发metadata回调
|
||||
└→ VideoEngine.load()
|
||||
├→ Preflight检查(可选)
|
||||
├→ 创建Input
|
||||
├→ 获取视频轨道
|
||||
├→ 创建CanvasSink
|
||||
├→ 初始化iterator
|
||||
└→ 触发metadata回调
|
||||
```
|
||||
|
||||
### 播放流程
|
||||
```
|
||||
MediaBunnyEngine.play()
|
||||
├→ AudioEngine.play()
|
||||
│ ├→ 恢复/创建AudioContext
|
||||
│ └→ 启动音频buffer迭代器
|
||||
└→ VideoEngine.start(audioEngine)
|
||||
├→ 保存audio时钟引用
|
||||
└→ 启动渲染循环(requestAnimationFrame)
|
||||
```
|
||||
|
||||
### 渲染循环
|
||||
```
|
||||
VideoEngine.render()
|
||||
├→ 获取当前音频时间
|
||||
├→ 发送timeupdate事件
|
||||
├→ 检查是否到达结束
|
||||
├→ 如果nextFrame时间到了
|
||||
│ ├→ 清除canvas
|
||||
│ ├→ 绘制frame
|
||||
│ └→ 请求下一帧
|
||||
└→ requestAnimationFrame(render)
|
||||
```
|
||||
|
||||
## 重构改进点
|
||||
|
||||
### 1. 代码组织
|
||||
- ✅ 扁平化目录结构(移除engine/shim/utils子目录)
|
||||
- ✅ 使用ES6类代替工厂函数
|
||||
- ✅ 每个文件单一职责
|
||||
|
||||
### 2. 可读性
|
||||
- ✅ 添加详细的JSDoc注释
|
||||
- ✅ 有意义的变量和方法命名
|
||||
- ✅ 逻辑分组和代码组织
|
||||
|
||||
### 3. 可维护性
|
||||
- ✅ 清晰的依赖关系
|
||||
- ✅ 统一的错误处理
|
||||
- ✅ 一致的代码风格
|
||||
|
||||
### 4. 性能
|
||||
- ✅ 优化的帧同步算法
|
||||
- ✅ 改进的缓冲区监控
|
||||
- ✅ 减少不必要的函数创建
|
||||
|
||||
### 5. 功能
|
||||
- ✅ 移除未使用的工具函数(sleep.js)
|
||||
- ✅ 简化事件系统
|
||||
- ✅ 统一的配置选项
|
||||
|
||||
## 使用的外部库
|
||||
|
||||
- **mediabunny**: WebCodecs封装库,提供Input/Source/Sink抽象
|
||||
- **ArtPlayer**: 视频播放器框架
|
||||
|
||||
## 浏览器API
|
||||
|
||||
- **WebCodecs API**: 视频/音频解码
|
||||
- **Web Audio API**: 音频播放
|
||||
- **Canvas API**: 视频渲染
|
||||
- **requestAnimationFrame**: 渲染循环
|
||||
@@ -1,11 +1,70 @@
|
||||
# artplayer-proxy-mediabunny
|
||||
|
||||
mediabunny proxy for ArtPlayer
|
||||
MediaBunny proxy for ArtPlayer - A high-performance video playback solution using WebCodecs and Canvas.
|
||||
|
||||
## Features
|
||||
|
||||
- 🚀 Hardware-accelerated video decoding via WebCodecs API
|
||||
- 🎨 Canvas-based rendering for maximum flexibility
|
||||
- 🎵 Web Audio API for audio playback
|
||||
- 🔄 Precise audio-video synchronization
|
||||
- 📦 Support for multiple media formats
|
||||
- ⚡ Optimized for modern browsers
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install artplayer-proxy-mediabunny
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import Artplayer from 'artplayer'
|
||||
import artplayerProxyMediabunny from 'artplayer-proxy-mediabunny'
|
||||
|
||||
const art = new Artplayer({
|
||||
container: '.artplayer-app',
|
||||
url: 'path/to/video.mp4',
|
||||
customType: {
|
||||
mediabunny: artplayerProxyMediabunny({
|
||||
// Options
|
||||
loadTimeout: 12000,
|
||||
timeupdateInterval: 250,
|
||||
avSyncTolerance: 0.12,
|
||||
dropLateFrames: false,
|
||||
poster: 'path/to/poster.jpg',
|
||||
}),
|
||||
},
|
||||
type: 'mediabunny',
|
||||
})
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `loadTimeout` | `number` | `12000` | Timeout for loading media (ms) |
|
||||
| `timeupdateInterval` | `number` | `250` | Interval for timeupdate events (ms) |
|
||||
| `avSyncTolerance` | `number` | `0.12` | Audio-video sync tolerance (seconds) |
|
||||
| `dropLateFrames` | `boolean` | `false` | Drop late video frames |
|
||||
| `poster` | `string` | `''` | Poster image URL |
|
||||
| `source` | `string\|Blob\|ReadableStream` | - | Media source |
|
||||
| `preflightRange` | `boolean` | `false` | Check range support before loading |
|
||||
| `volume` | `number` | `0.7` | Initial volume (0-1) |
|
||||
| `muted` | `boolean` | `false` | Initial muted state |
|
||||
|
||||
## Demo
|
||||
|
||||
[https://artplayer.org](https://artplayer.org/?libs=./uncompiled/artplayer-proxy-mediabunny/index.js&example=mediabunny)
|
||||
|
||||
## Browser Support
|
||||
|
||||
Requires browsers with WebCodecs API support:
|
||||
- Chrome 94+
|
||||
- Edge 94+
|
||||
- Opera 80+
|
||||
|
||||
## License
|
||||
|
||||
MIT © Harvey Zack
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
/*!
|
||||
* artplayer-proxy-mediabunny.js v1.0.0
|
||||
* Github: https://github.com/zhw2590582/ArtPlayer
|
||||
* (c) 2017-2025 Harvey Zack
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,r,a,i,s,n,o,l){var c="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},d="function"==typeof c[i]&&c[i],u=d.i||{},h=d.cache||{},m="undefined"!=typeof module&&"function"==typeof module.require&&module.require.bind(module);function p(r,a){if(!h[r]){if(!e[r]){if(s[r])return s[r];var n="function"==typeof c[i]&&c[i];if(!a&&n)return n(r,!0);if(d)return d(r,!0);if(m&&"string"==typeof r)return m(r);var o=Error("Cannot find module '"+r+"'");throw o.code="MODULE_NOT_FOUND",o}u.resolve=function(a){var i=e[r][1][a];return null!=i?i:a},u.cache={};var l=h[r]=new p.Module(r);e[r][0].call(l.exports,u,l,l.exports,c)}return h[r].exports;function u(e){var r=u.resolve(e);if(!1===r)return{};if(Array.isArray(r)){var a={__esModule:!0};return r.forEach(function(e){var r=e[0],i=e[1],s=e[2]||e[0],n=p(i);"*"===r?Object.keys(n).forEach(function(e){"default"===e||"__esModule"===e||Object.prototype.hasOwnProperty.call(a,e)||Object.defineProperty(a,e,{enumerable:!0,get:function(){return n[e]}})}):"*"===s?Object.defineProperty(a,r,{enumerable:!0,value:n}):Object.defineProperty(a,r,{enumerable:!0,get:function(){return"default"===s?n.__esModule?n.default:n:n[s]}})}),a}return p(r)}}p.isParcelRequire=!0,p.Module=function(e){this.id=e,this.bundle=p,this.require=m,this.exports={}},p.modules=e,p.cache=h,p.parent=d,p.distDir=void 0,p.publicUrl=void 0,p.devServer=void 0,p.i=u,p.register=function(r,a){e[r]=[function(e,r){r.exports=a},{}]},Object.defineProperty(p,"root",{get:function(){return c[i]}}),c[i]=p;for(var f=0;f<r.length;f++)p(r[f]);if(a){var g=p(a);"object"==typeof exports&&"undefined"!=typeof module?module.exports=g:"function"==typeof define&&define.amd&&define(function(){return g})}}({"1qMFK":[function(e,r,a,i){var s=e("@parcel/transformer-js/src/esmodule-helpers.js");s.defineInteropFlag(a),s.export(a,"default",()=>l);var n=e("./proxy/canvasProxy.js"),o=s.interopDefault(n);function l(e={}){return r=>(0,o.default)(r,e)}"undefined"!=typeof window&&(window.artplayerProxyMediabunny=l)},{"./proxy/canvasProxy.js":"ln3T4","@parcel/transformer-js/src/esmodule-helpers.js":"6ykb1"}],ln3T4:[function(e,r,a,i){var s=e("@parcel/transformer-js/src/esmodule-helpers.js");s.defineInteropFlag(a),s.export(a,"default",()=>l);var n=e("../shim/mediabunnyVideoShim.js"),o=s.interopDefault(n);function l(e,r){let{constructor:a}=e,{createElement:i}=a.utils,s=i("canvas"),n=s.getContext("2d"),l=(0,o.default)({art:e,canvas:s,ctx:n,option:r}),c={};for(let e in s)"function"==typeof s[e]&&(c[e]=s[e].bind(s));for(let e in l)e in s||Object.defineProperty(s,e,{get(){let r=l[e];return"function"==typeof r?r.bind(l):r},set(r){l[e]=r},configurable:!0,enumerable:!0});for(let e in c)s[e]=(...r)=>c[e](...r);function d(){e.template?.$player&&!e.option.autoSize&&Object.assign(s.style,{width:"100%",height:"100%",objectFit:"contain"})}return e.on("destroy",()=>{l.destroy()}),e.on("resize",d),e.on("video:loadedmetadata",d),s}},{"../shim/mediabunnyVideoShim.js":"aLQUP","@parcel/transformer-js/src/esmodule-helpers.js":"6ykb1"}],aLQUP:[function(e,r,a,i){var s=e("@parcel/transformer-js/src/esmodule-helpers.js");s.defineInteropFlag(a),s.export(a,"default",()=>u);var n=e("../engine/mediabunnyEngine.js"),o=s.interopDefault(n),l=e("../utils/clamp.js"),c=s.interopDefault(l),d=e("./videoEvents.js");function u({art:e,canvas:r,ctx:a,option:i}){let s=(0,d.createVideoEventTarget)(),n=(0,o.default)({canvas:r,ctx:a,events:s,option:i}),l=null,u=i.volume??.7,h=!!i.muted,m=1;n.setVolume(u,h);let{events:p}=e.constructor.config;p.forEach(r=>{s.addEventListener(r,r=>{e.emit(`video:${r.type}`,r)})});let f={addEventListener:s.addEventListener,removeEventListener:s.removeEventListener,get src(){return l},set src(v){l=v,v&&n.load(v)},get currentTime(){return n.currentTime},set currentTime(t){n.seek(Number(t)||0)},get duration(){return n.duration},get buffered(){let e=n.duration;if(!e||Number.isNaN(e))return{length:0,start:()=>0,end:()=>0};return{length:1,start:()=>0,end:()=>e}},get played(){let e=n.duration,r=n.currentTime;if(!e||Number.isNaN(e)||r<=0)return{length:0,start:()=>0,end:()=>0};return{length:1,start:()=>0,end:()=>r}},get paused(){return n.paused},get playing(){return!n.paused&&!n.ended},get ended(){return n.ended},get readyState(){return n.readyState},get networkState(){return n.networkState},get error(){return n.error},get seeking(){return n.seeking},get seekable(){let e=n.duration;if(!e||Number.isNaN(e))return{length:0,start:()=>0,end:()=>0};return{length:1,start:()=>0,end:()=>e}},get playbackRate(){return m},set playbackRate(v){let e=Number(v);if(Number.isNaN(e)||e<=0)return;m=e,n.setPlaybackRate(e),s.emit("ratechange")},get volume(){return u},set volume(v){u=(0,c.default)(v,0,1),h=!1,n.setVolume(u,h),s.emit("volumechange")},get muted(){return h},set muted(v){h=!!v,n.setVolume(u,h),s.emit("volumechange")},play:()=>n.play(),pause(){n.pause()},load(){l&&n.load(l)},destroy(){n.destroy()},get videLine truncated
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
/*!
|
||||
* artplayer-proxy-mediabunny.js v1.0.0
|
||||
* Github: https://github.com/zhw2590582/ArtPlayer
|
||||
* (c) 2017-2025 Harvey Zack
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,r,n,a,i,s,o,c){var u="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},l="function"==typeof u[a]&&u[a],d=l.i||{},f=l.cache||{},h="undefined"!=typeof module&&"function"==typeof module.require&&module.require.bind(module);function m(r,n){if(!f[r]){if(!e[r]){if(i[r])return i[r];var s="function"==typeof u[a]&&u[a];if(!n&&s)return s(r,!0);if(l)return l(r,!0);if(h&&"string"==typeof r)return h(r);var o=Error("Cannot find module '"+r+"'");throw o.code="MODULE_NOT_FOUND",o}d.resolve=function(n){var a=e[r][1][n];return null!=a?a:n},d.cache={};var c=f[r]=new m.Module(r);e[r][0].call(c.exports,d,c,c.exports,u)}return f[r].exports;function d(e){var r=d.resolve(e);if(!1===r)return{};if(Array.isArray(r)){var n={__esModule:!0};return r.forEach(function(e){var r=e[0],a=e[1],i=e[2]||e[0],s=m(a);"*"===r?Object.keys(s).forEach(function(e){"default"===e||"__esModule"===e||Object.prototype.hasOwnProperty.call(n,e)||Object.defineProperty(n,e,{enumerable:!0,get:function(){return s[e]}})}):"*"===i?Object.defineProperty(n,r,{enumerable:!0,value:s}):Object.defineProperty(n,r,{enumerable:!0,get:function(){return"default"===i?s.__esModule?s.default:s:s[i]}})}),n}return m(r)}}m.isParcelRequire=!0,m.Module=function(e){this.id=e,this.bundle=m,this.require=h,this.exports={}},m.modules=e,m.cache=f,m.parent=l,m.distDir=void 0,m.publicUrl=void 0,m.devServer=void 0,m.i=d,m.register=function(r,n){e[r]=[function(e,r){r.exports=n},{}]},Object.defineProperty(m,"root",{get:function(){return u[a]}}),u[a]=m;for(var p=0;p<r.length;p++)m(r[p]);if(n){var g=m(n);"object"==typeof exports&&"undefined"!=typeof module?module.exports=g:"function"==typeof define&&define.amd&&define(function(){return g})}}({OIaIB:[function(e,r,n,a){var i=e("@parcel/transformer-js/src/esmodule-helpers.js");i.defineInteropFlag(n),i.export(n,"default",function(){return c});var s=e("./proxy/canvasProxy.js"),o=i.interopDefault(s);function c(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return function(r){return(0,o.default)(r,e)}}"undefined"!=typeof window&&(window.artplayerProxyMediabunny=c)},{"./proxy/canvasProxy.js":"hTxiw","@parcel/transformer-js/src/esmodule-helpers.js":"qpMMf"}],hTxiw:[function(e,r,n,a){var i=e("@parcel/transformer-js/src/esmodule-helpers.js");i.defineInteropFlag(n),i.export(n,"default",function(){return u});var s=e("@swc/helpers/cjs/_to_consumable_array.cjs"),o=e("../shim/mediabunnyVideoShim.js"),c=i.interopDefault(o);function u(e,r){var n,a=function(e){e in o||Object.defineProperty(o,e,{get:function(){var r=l[e];return"function"==typeof r?r.bind(l):r},set:function(r){l[e]=r},configurable:!0,enumerable:!0})},i=function(e){o[e]=function(){for(var r=arguments.length,a=Array(r),i=0;i<r;i++)a[i]=arguments[i];return(n=d)[e].apply(n,(0,s._)(a))}},o=(0,e.constructor.utils.createElement)("canvas"),u=o.getContext("2d"),l=(0,c.default)({art:e,canvas:o,ctx:u,option:r}),d={};for(var f in o)"function"==typeof o[f]&&(d[f]=o[f].bind(o));for(var h in l)a(h);for(var m in d)i(m);function p(){var r;(null==(r=e.template)?void 0:r.$player)&&!e.option.autoSize&&Object.assign(o.style,{width:"100%",height:"100%",objectFit:"contain"})}return e.on("destroy",function(){l.destroy()}),e.on("resize",p),e.on("video:loadedmetadata",p),o}},{"@swc/helpers/cjs/_to_consumable_array.cjs":"i81N6","../shim/mediabunnyVideoShim.js":"4Z3fn","@parcel/transformer-js/src/esmodule-helpers.js":"qpMMf"}],i81N6:[function(e,r,n,a){"use strict";var i=e("d402183024004bc7"),s=e("3209e2b12672cd18"),o=e("fc58adb8f1bff142"),c=e("5e213dc122a10171");n._=function(e){return i._(e)||s._(e)||c._(e)||o._()}},{d402183024004bc7:"hfQG0","3209e2b12672cd18":"fQrlQ",fc58adb8f1bff142:"fWabh","5e213dc122a10171":"fh7mx"}],hfQG0:[function(e,r,n,a){"use strict";var i=e("5bd5b10bf87fc8e");n._=function(e){if(Array.isArray(e))return i._(e)}},{"5bd5b10bf87fc8e":"1WOAB"}],"1WOAB":[function(e,r,n,a){"use strict";n._=function(e,r){(null==r||r>e.length)&&(r=e.length);for(var n=0,a=Array(r);n<r;n++)a[n]=e[n];return a}},{}],fQrlQ:[function(e,r,n,a){"use strict";n._=function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}},{}],fWabh:[function(e,r,n,a){"use strict";n._=function(){throw TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}},{}],fh7mx:[function(e,r,n,a){"use strict";var i=e("3ee51b7dc02d0c44");n._=function(e,r){if(e){if("string"==typeof e)return i._(e,r);var n=Object.prototype.toString.call(e).slice(8,-1);if("Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n)return Array.from(n);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return i._(e,r)}}},{"3ee51b7dc02d0c44":"1WOAB"}],"4Z3fn":[function(e,r,n,a){var i=e("@parcel/transformer-js/src/esmodule-helpers.js");i.defineInteropFlag(n),i.export(n,"default",function(){retLine truncated
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
/*!
|
||||
* artplayer-proxy-mediabunny.js v1.0.0
|
||||
* Github: https://github.com/zhw2590582/ArtPlayer
|
||||
* (c) 2017-2025 Harvey Zack
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,r,a,i,s,n,o,l){var c="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},d="function"==typeof c[i]&&c[i],u=d.i||{},h=d.cache||{},m="undefined"!=typeof module&&"function"==typeof module.require&&module.require.bind(module);function p(r,a){if(!h[r]){if(!e[r]){if(s[r])return s[r];var n="function"==typeof c[i]&&c[i];if(!a&&n)return n(r,!0);if(d)return d(r,!0);if(m&&"string"==typeof r)return m(r);var o=Error("Cannot find module '"+r+"'");throw o.code="MODULE_NOT_FOUND",o}u.resolve=function(a){var i=e[r][1][a];return null!=i?i:a},u.cache={};var l=h[r]=new p.Module(r);e[r][0].call(l.exports,u,l,l.exports,c)}return h[r].exports;function u(e){var r=u.resolve(e);if(!1===r)return{};if(Array.isArray(r)){var a={__esModule:!0};return r.forEach(function(e){var r=e[0],i=e[1],s=e[2]||e[0],n=p(i);"*"===r?Object.keys(n).forEach(function(e){"default"===e||"__esModule"===e||Object.prototype.hasOwnProperty.call(a,e)||Object.defineProperty(a,e,{enumerable:!0,get:function(){return n[e]}})}):"*"===s?Object.defineProperty(a,r,{enumerable:!0,value:n}):Object.defineProperty(a,r,{enumerable:!0,get:function(){return"default"===s?n.__esModule?n.default:n:n[s]}})}),a}return p(r)}}p.isParcelRequire=!0,p.Module=function(e){this.id=e,this.bundle=p,this.require=m,this.exports={}},p.modules=e,p.cache=h,p.parent=d,p.distDir=void 0,p.publicUrl=void 0,p.devServer=void 0,p.i=u,p.register=function(r,a){e[r]=[function(e,r){r.exports=a},{}]},Object.defineProperty(p,"root",{get:function(){return c[i]}}),c[i]=p;for(var f=0;f<r.length;f++)p(r[f]);if(a){var g=p(a);"object"==typeof exports&&"undefined"!=typeof module?module.exports=g:"function"==typeof define&&define.amd&&define(function(){return g})}}({"2x2Ir":[function(e,r,a,i){var s=e("@parcel/transformer-js/src/esmodule-helpers.js");s.defineInteropFlag(a),s.export(a,"default",()=>l);var n=e("./proxy/canvasProxy.js"),o=s.interopDefault(n);function l(e={}){return r=>(0,o.default)(r,e)}"undefined"!=typeof window&&(window.artplayerProxyMediabunny=l)},{"./proxy/canvasProxy.js":"dQEB4","@parcel/transformer-js/src/esmodule-helpers.js":"loqXi"}],dQEB4:[function(e,r,a,i){var s=e("@parcel/transformer-js/src/esmodule-helpers.js");s.defineInteropFlag(a),s.export(a,"default",()=>l);var n=e("../shim/mediabunnyVideoShim.js"),o=s.interopDefault(n);function l(e,r){let{constructor:a}=e,{createElement:i}=a.utils,s=i("canvas"),n=s.getContext("2d"),l=(0,o.default)({art:e,canvas:s,ctx:n,option:r}),c={};for(let e in s)"function"==typeof s[e]&&(c[e]=s[e].bind(s));for(let e in l)e in s||Object.defineProperty(s,e,{get(){let r=l[e];return"function"==typeof r?r.bind(l):r},set(r){l[e]=r},configurable:!0,enumerable:!0});for(let e in c)s[e]=(...r)=>c[e](...r);function d(){e.template?.$player&&!e.option.autoSize&&Object.assign(s.style,{width:"100%",height:"100%",objectFit:"contain"})}return e.on("destroy",()=>{l.destroy()}),e.on("resize",d),e.on("video:loadedmetadata",d),s}},{"../shim/mediabunnyVideoShim.js":"1TXLF","@parcel/transformer-js/src/esmodule-helpers.js":"loqXi"}],"1TXLF":[function(e,r,a,i){var s=e("@parcel/transformer-js/src/esmodule-helpers.js");s.defineInteropFlag(a),s.export(a,"default",()=>u);var n=e("../engine/mediabunnyEngine.js"),o=s.interopDefault(n),l=e("../utils/clamp.js"),c=s.interopDefault(l),d=e("./videoEvents.js");function u({art:e,canvas:r,ctx:a,option:i}){let s=(0,d.createVideoEventTarget)(),n=(0,o.default)({canvas:r,ctx:a,events:s,option:i}),l=null,u=i.volume??.7,h=!!i.muted,m=1;n.setVolume(u,h);let{events:p}=e.constructor.config;p.forEach(r=>{s.addEventListener(r,r=>{e.emit(`video:${r.type}`,r)})});let f={addEventListener:s.addEventListener,removeEventListener:s.removeEventListener,get src(){return l},set src(v){l=v,v&&n.load(v)},get currentTime(){return n.currentTime},set currentTime(t){n.seek(Number(t)||0)},get duration(){return n.duration},get buffered(){let e=n.duration;if(!e||Number.isNaN(e))return{length:0,start:()=>0,end:()=>0};return{length:1,start:()=>0,end:()=>e}},get played(){let e=n.duration,r=n.currentTime;if(!e||Number.isNaN(e)||r<=0)return{length:0,start:()=>0,end:()=>0};return{length:1,start:()=>0,end:()=>r}},get paused(){return n.paused},get playing(){return!n.paused&&!n.ended},get ended(){return n.ended},get readyState(){return n.readyState},get networkState(){return n.networkState},get error(){return n.error},get seeking(){return n.seeking},get seekable(){let e=n.duration;if(!e||Number.isNaN(e))return{length:0,start:()=>0,end:()=>0};return{length:1,start:()=>0,end:()=>e}},get playbackRate(){return m},set playbackRate(v){let e=Number(v);if(Number.isNaN(e)||e<=0)return;m=e,n.setPlaybackRate(e),s.emit("ratechange")},get volume(){return u},set volume(v){u=(0,c.default)(v,0,1),h=!1,n.setVolume(u,h),s.emit("volumechange")},get muted(){return h},set muted(v){h=!!v,n.setVolume(u,h),s.emit("volumechange")},play:()=>n.play(),pause(){n.pause()},load(){l&&n.load(l)},destroy(){n.destroy()},get viLine truncated
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* Audio Engine for MediaBunny
|
||||
* Handles audio playback using Web Audio API
|
||||
*/
|
||||
import {
|
||||
ALL_FORMATS,
|
||||
AudioBufferSink,
|
||||
BlobSource,
|
||||
Input,
|
||||
ReadableStreamSource,
|
||||
UrlSource,
|
||||
} from 'mediabunny'
|
||||
|
||||
export default class AudioEngine {
|
||||
constructor(events) {
|
||||
this.events = events
|
||||
|
||||
// MediaBunny instances
|
||||
this.input = null
|
||||
this.audioSink = null
|
||||
this.audioIterator = null
|
||||
|
||||
// Web Audio API
|
||||
this.audioContext = null
|
||||
this.gainNode = null
|
||||
|
||||
// Playback state
|
||||
this.audioContextStartTime = 0
|
||||
this.playbackTimeAtStart = 0
|
||||
this.latestScheduledEndTime = 0
|
||||
this.duration = Number.NaN
|
||||
this.paused = true
|
||||
|
||||
// Audio settings
|
||||
this.volume = 0.7
|
||||
this.muted = false
|
||||
this.playbackRate = 1
|
||||
|
||||
// Async control
|
||||
this.asyncId = 0
|
||||
this.queuedNodes = new Set()
|
||||
}
|
||||
|
||||
get currentTime() {
|
||||
if (this.paused) return this.playbackTimeAtStart
|
||||
|
||||
return (
|
||||
(this.audioContext.currentTime - this.audioContextStartTime) * this.playbackRate
|
||||
+ this.playbackTimeAtStart
|
||||
)
|
||||
}
|
||||
|
||||
normalizeSource(src) {
|
||||
if (typeof src === 'string') return new UrlSource(src)
|
||||
if (src instanceof Blob) return new BlobSource(src)
|
||||
if (typeof ReadableStream !== 'undefined' && src instanceof ReadableStream) {
|
||||
return new ReadableStreamSource(src)
|
||||
}
|
||||
return src
|
||||
}
|
||||
|
||||
ensureAudioContext(sampleRate) {
|
||||
if (this.audioContext) return
|
||||
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext
|
||||
|
||||
try {
|
||||
this.audioContext = new AudioContext({ sampleRate })
|
||||
} catch {
|
||||
this.audioContext = new AudioContext()
|
||||
}
|
||||
|
||||
this.gainNode = this.audioContext.createGain()
|
||||
this.gainNode.connect(this.audioContext.destination)
|
||||
this.updateGain()
|
||||
}
|
||||
|
||||
updateGain() {
|
||||
if (!this.gainNode) return
|
||||
const v = this.muted ? 0 : this.volume
|
||||
this.gainNode.gain.value = v * v
|
||||
}
|
||||
|
||||
stopQueuedNodes() {
|
||||
this.queuedNodes.forEach(node => node.stop())
|
||||
this.queuedNodes.clear()
|
||||
}
|
||||
|
||||
async stopIterator() {
|
||||
await this.audioIterator?.return()
|
||||
this.audioIterator = null
|
||||
}
|
||||
|
||||
async load(src, onMetadata) {
|
||||
const id = ++this.asyncId
|
||||
|
||||
await this.stopIterator()
|
||||
this.stopQueuedNodes()
|
||||
|
||||
this.paused = true
|
||||
this.playbackTimeAtStart = 0
|
||||
this.audioContextStartTime = 0
|
||||
|
||||
const source = this.normalizeSource(src)
|
||||
if (!source) return
|
||||
|
||||
this.input = new Input({
|
||||
source,
|
||||
formats: ALL_FORMATS,
|
||||
})
|
||||
|
||||
this.duration = await this.input.computeDuration()
|
||||
if (id !== this.asyncId) return
|
||||
|
||||
const audioTrack = await this.input.getPrimaryAudioTrack()
|
||||
if (!audioTrack) {
|
||||
this.audioSink = null
|
||||
this.ensureAudioContext()
|
||||
onMetadata?.()
|
||||
return
|
||||
}
|
||||
|
||||
if (audioTrack.codec === null || !(await audioTrack.canDecode())) {
|
||||
this.audioSink = null
|
||||
this.ensureAudioContext()
|
||||
onMetadata?.()
|
||||
return
|
||||
}
|
||||
|
||||
this.ensureAudioContext(audioTrack.sampleRate)
|
||||
this.audioSink = new AudioBufferSink(audioTrack)
|
||||
|
||||
onMetadata?.()
|
||||
}
|
||||
|
||||
async runIterator(localId) {
|
||||
if (!this.audioSink) return
|
||||
|
||||
await this.stopIterator()
|
||||
this.audioIterator = this.audioSink.buffers(this.currentTime)
|
||||
|
||||
while (true) {
|
||||
if (localId !== this.asyncId || this.paused) return
|
||||
|
||||
const nextPromise = this.audioIterator.next()
|
||||
|
||||
// Monitor for buffer starvation
|
||||
const checkStarvation = setInterval(() => {
|
||||
if (localId !== this.asyncId || this.paused) {
|
||||
clearInterval(checkStarvation)
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
this.audioContext.state === 'running' &&
|
||||
this.audioContext.currentTime >= this.latestScheduledEndTime - 0.2
|
||||
) {
|
||||
this.audioContext.suspend()
|
||||
this.events.emit('waiting')
|
||||
}
|
||||
}, 50)
|
||||
|
||||
let result
|
||||
try {
|
||||
result = await nextPromise
|
||||
} catch (e) {
|
||||
console.error('Audio iterator error:', e)
|
||||
break
|
||||
} finally {
|
||||
clearInterval(checkStarvation)
|
||||
}
|
||||
|
||||
if (localId !== this.asyncId || this.paused) return
|
||||
|
||||
// Resume if was suspended
|
||||
if (this.audioContext.state === 'suspended') {
|
||||
await this.audioContext.resume()
|
||||
this.events.emit('canplay')
|
||||
this.events.emit('playing')
|
||||
}
|
||||
|
||||
if (result.done) break
|
||||
|
||||
const { buffer, timestamp } = result.value
|
||||
|
||||
// Schedule audio buffer
|
||||
const node = this.audioContext.createBufferSource()
|
||||
node.buffer = buffer
|
||||
node.connect(this.gainNode)
|
||||
node.playbackRate.value = this.playbackRate
|
||||
|
||||
const startAt =
|
||||
this.audioContextStartTime +
|
||||
(timestamp - this.playbackTimeAtStart) / this.playbackRate
|
||||
|
||||
const duration = buffer.duration
|
||||
const endAt = startAt + duration / this.playbackRate
|
||||
|
||||
if (endAt > this.latestScheduledEndTime) {
|
||||
this.latestScheduledEndTime = endAt
|
||||
}
|
||||
|
||||
if (startAt >= this.audioContext.currentTime) {
|
||||
node.start(startAt)
|
||||
} else {
|
||||
node.start(
|
||||
this.audioContext.currentTime,
|
||||
(this.audioContext.currentTime - startAt) * this.playbackRate
|
||||
)
|
||||
}
|
||||
|
||||
this.queuedNodes.add(node)
|
||||
node.onended = () => this.queuedNodes.delete(node)
|
||||
}
|
||||
}
|
||||
|
||||
async play() {
|
||||
if (!this.paused) return
|
||||
|
||||
if (!this.audioContext) {
|
||||
this.ensureAudioContext()
|
||||
}
|
||||
|
||||
if (this.audioContext.state === 'suspended') {
|
||||
await this.audioContext.resume()
|
||||
}
|
||||
|
||||
this.audioContextStartTime = this.audioContext.currentTime
|
||||
this.latestScheduledEndTime = this.audioContextStartTime
|
||||
this.paused = false
|
||||
|
||||
const id = ++this.asyncId
|
||||
this.runIterator(id)
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (this.paused) return
|
||||
|
||||
this.playbackTimeAtStart = this.currentTime
|
||||
this.paused = true
|
||||
|
||||
this.stopIterator()
|
||||
this.stopQueuedNodes()
|
||||
}
|
||||
|
||||
async seek(time) {
|
||||
this.playbackTimeAtStart = Math.max(0, time)
|
||||
this.audioContextStartTime = this.audioContext.currentTime
|
||||
this.latestScheduledEndTime = this.audioContextStartTime
|
||||
|
||||
const id = ++this.asyncId
|
||||
if (!this.paused) {
|
||||
this.runIterator(id)
|
||||
}
|
||||
}
|
||||
|
||||
setVolume(volume, muted) {
|
||||
this.volume = volume
|
||||
this.muted = muted
|
||||
this.updateGain()
|
||||
}
|
||||
|
||||
setPlaybackRate(rate) {
|
||||
if (rate === this.playbackRate) return
|
||||
|
||||
if (!this.paused) {
|
||||
this.playbackTimeAtStart = this.currentTime
|
||||
this.audioContextStartTime = this.audioContext.currentTime
|
||||
}
|
||||
|
||||
this.playbackRate = rate
|
||||
|
||||
if (!this.paused) {
|
||||
const id = ++this.asyncId
|
||||
this.runIterator(id)
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.asyncId++
|
||||
this.pause()
|
||||
this.audioContext?.close()
|
||||
this.audioContext = null
|
||||
this.input = null
|
||||
this.audioSink = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Event Target Implementation
|
||||
* Simple event system for video events
|
||||
*/
|
||||
export default class EventTarget {
|
||||
constructor() {
|
||||
this.listeners = new Map()
|
||||
}
|
||||
|
||||
addEventListener(type, fn) {
|
||||
if (!this.listeners.has(type)) {
|
||||
this.listeners.set(type, [])
|
||||
}
|
||||
this.listeners.get(type).push(fn)
|
||||
}
|
||||
|
||||
removeEventListener(type, fn) {
|
||||
const list = this.listeners.get(type)
|
||||
if (!list) return
|
||||
|
||||
const index = list.indexOf(fn)
|
||||
if (index >= 0) {
|
||||
list.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
emit(type, detail) {
|
||||
const evt = new Event(type)
|
||||
evt.detail = detail
|
||||
|
||||
const list = this.listeners.get(type)
|
||||
if (list) {
|
||||
list.forEach(fn => fn(evt))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Main MediaBunny Engine
|
||||
* Coordinates audio and video playback
|
||||
*/
|
||||
import AudioEngine from './AudioEngine.js'
|
||||
import VideoEngine from './VideoEngine.js'
|
||||
|
||||
export default class MediaBunnyEngine {
|
||||
constructor({ canvas, ctx, events, option = {} }) {
|
||||
this.events = events
|
||||
this.option = option
|
||||
|
||||
// Create audio and video engines
|
||||
this.audio = new AudioEngine(events)
|
||||
this.video = new VideoEngine({
|
||||
canvas,
|
||||
ctx,
|
||||
events,
|
||||
timeupdateInterval: option.timeupdateInterval ?? 250,
|
||||
avSyncTolerance: option.avSyncTolerance ?? 0.12,
|
||||
dropLateFrames: option.dropLateFrames ?? false,
|
||||
poster: option.poster ?? '',
|
||||
preflightRange: option.preflightRange ?? false,
|
||||
})
|
||||
|
||||
// Playback state
|
||||
this.paused = true
|
||||
this.ended = false
|
||||
this.readyState = 0
|
||||
this.networkState = 0
|
||||
this.error = null
|
||||
this.seeking = false
|
||||
this.loadSeq = 0
|
||||
|
||||
// Listen to ended event
|
||||
events.addEventListener?.('ended', () => {
|
||||
this.ended = true
|
||||
this.paused = true
|
||||
})
|
||||
}
|
||||
|
||||
async load(src) {
|
||||
const id = ++this.loadSeq
|
||||
|
||||
this.pause()
|
||||
this.ended = false
|
||||
this.error = null
|
||||
this.networkState = 2 // NETWORK_LOADING
|
||||
this.readyState = 0 // HAVE_NOTHING
|
||||
|
||||
setTimeout(() => this.events.emit('waiting'), 0)
|
||||
setTimeout(() => this.events.emit('loadstart'), 0)
|
||||
|
||||
const loadTimeout = Number.isFinite(this.option.loadTimeout)
|
||||
? this.option.loadTimeout
|
||||
: 12000
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
this.performLoad(src, id),
|
||||
loadTimeout > 0 ? this.createTimeout(loadTimeout) : Promise.resolve(),
|
||||
])
|
||||
} catch (err) {
|
||||
if (id !== this.loadSeq) return
|
||||
|
||||
this.loadSeq++
|
||||
this.error = { code: 4, message: err.message }
|
||||
this.networkState = 3 // NETWORK_NO_SOURCE
|
||||
this.events.emit('error')
|
||||
}
|
||||
}
|
||||
|
||||
async performLoad(src, id) {
|
||||
let videoMetadataLoaded = false
|
||||
let audioMetadataLoaded = false
|
||||
|
||||
const checkMetadata = () => {
|
||||
if (videoMetadataLoaded && audioMetadataLoaded) {
|
||||
this.readyState = 1 // HAVE_METADATA
|
||||
this.events.emit('loadedmetadata')
|
||||
this.events.emit('durationchange')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
this.video.load(src, () => {
|
||||
if (id !== this.loadSeq) return
|
||||
videoMetadataLoaded = true
|
||||
checkMetadata()
|
||||
}),
|
||||
this.audio.load(src, () => {
|
||||
if (id !== this.loadSeq) return
|
||||
audioMetadataLoaded = true
|
||||
checkMetadata()
|
||||
}),
|
||||
])
|
||||
|
||||
if (id !== this.loadSeq) return
|
||||
|
||||
this.readyState = 4 // HAVE_ENOUGH_DATA
|
||||
this.networkState = 1 // NETWORK_IDLE
|
||||
this.events.emit('loadeddata')
|
||||
this.events.emit('canplay')
|
||||
this.events.emit('canplaythrough')
|
||||
} catch (err) {
|
||||
if (id !== this.loadSeq) return
|
||||
|
||||
this.error = { code: 4, message: err.message }
|
||||
this.networkState = 3
|
||||
this.events.emit('error')
|
||||
console.error('MediaBunny load error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
createTimeout(ms) {
|
||||
return new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Load timeout')), ms)
|
||||
})
|
||||
}
|
||||
|
||||
async play() {
|
||||
if (!this.paused) return
|
||||
|
||||
if (this.ended) {
|
||||
this.ended = false
|
||||
await this.seek(0)
|
||||
}
|
||||
|
||||
this.paused = false
|
||||
|
||||
await this.audio.play()
|
||||
this.video.start(this.audio)
|
||||
|
||||
this.events.emit('play')
|
||||
this.events.emit('playing')
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (this.paused) return
|
||||
|
||||
this.paused = true
|
||||
|
||||
this.audio.pause()
|
||||
this.video.stop()
|
||||
|
||||
this.events.emit('pause')
|
||||
}
|
||||
|
||||
async seek(time) {
|
||||
const shouldResume = !this.paused
|
||||
|
||||
this.ended = false
|
||||
this.seeking = true
|
||||
|
||||
this.events.emit('seeking')
|
||||
this.events.emit('waiting')
|
||||
|
||||
this.pause()
|
||||
|
||||
await Promise.all([
|
||||
this.audio.seek(time),
|
||||
this.video.seek(time),
|
||||
])
|
||||
|
||||
this.seeking = false
|
||||
this.events.emit('seeked')
|
||||
|
||||
if (shouldResume && !this.ended) {
|
||||
await this.play()
|
||||
}
|
||||
}
|
||||
|
||||
setVolume(volume, muted) {
|
||||
this.audio.setVolume(volume, muted)
|
||||
}
|
||||
|
||||
setPlaybackRate(rate) {
|
||||
this.audio.setPlaybackRate(rate)
|
||||
this.video.setPlaybackRate(rate)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.pause()
|
||||
this.audio.destroy()
|
||||
this.video.destroy()
|
||||
}
|
||||
|
||||
// Getters
|
||||
get currentTime() {
|
||||
return this.audio.currentTime
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return this.audio.duration || this.video.duration
|
||||
}
|
||||
|
||||
get videoWidth() {
|
||||
return this.video.width
|
||||
}
|
||||
|
||||
get videoHeight() {
|
||||
return this.video.height
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* Video Engine for MediaBunny
|
||||
* Handles video frame rendering and synchronization
|
||||
*/
|
||||
import {
|
||||
ALL_FORMATS,
|
||||
BlobSource,
|
||||
CanvasSink,
|
||||
Input,
|
||||
ReadableStreamSource,
|
||||
UrlSource,
|
||||
} from 'mediabunny'
|
||||
|
||||
export default class VideoEngine {
|
||||
constructor({
|
||||
canvas,
|
||||
ctx,
|
||||
events,
|
||||
timeupdateInterval = 250,
|
||||
avSyncTolerance = 0.12,
|
||||
dropLateFrames = false,
|
||||
poster = '',
|
||||
preflightRange = false,
|
||||
}) {
|
||||
this.canvas = canvas
|
||||
this.ctx = ctx
|
||||
this.events = events
|
||||
this.timeupdateInterval = timeupdateInterval
|
||||
this.avSyncTolerance = avSyncTolerance
|
||||
this.dropLateFrames = dropLateFrames
|
||||
this.poster = poster
|
||||
this.preflightRange = preflightRange
|
||||
|
||||
// MediaBunny instances
|
||||
this.input = null
|
||||
this.videoSink = null
|
||||
this.videoIterator = null
|
||||
|
||||
// Frame rendering
|
||||
this.nextFrame = null
|
||||
this.rafId = 0
|
||||
this.asyncId = 0
|
||||
|
||||
// Video properties
|
||||
this.width = 0
|
||||
this.height = 0
|
||||
this.duration = Number.NaN
|
||||
|
||||
// Playback state
|
||||
this.audioClock = null
|
||||
this.lastTimeUpdate = 0
|
||||
this.stalled = false
|
||||
this.playbackRate = 1
|
||||
this.posterDrawn = false
|
||||
}
|
||||
|
||||
normalizeSource(src) {
|
||||
if (typeof src === 'string') return new UrlSource(src)
|
||||
if (src instanceof Blob) return new BlobSource(src)
|
||||
if (typeof ReadableStream !== 'undefined' && src instanceof ReadableStream) {
|
||||
return new ReadableStreamSource(src)
|
||||
}
|
||||
return src
|
||||
}
|
||||
|
||||
async preflight(url) {
|
||||
if (!this.preflightRange || typeof url !== 'string') return true
|
||||
|
||||
try {
|
||||
const res = await fetch(url, { method: 'HEAD' })
|
||||
const acceptRanges = res.headers.get('accept-ranges')
|
||||
if (!acceptRanges || acceptRanges === 'none') {
|
||||
this.events.emit('error', new Event('RangeNotSupported'))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} catch (e) {
|
||||
console.warn('Preflight check failed:', e)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
drawPoster() {
|
||||
if (!this.poster || this.posterDrawn) return
|
||||
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
this.canvas.width = img.naturalWidth || this.canvas.width
|
||||
this.canvas.height = img.naturalHeight || this.canvas.height
|
||||
this.ctx.drawImage(img, 0, 0, this.canvas.width, this.canvas.height)
|
||||
this.posterDrawn = true
|
||||
}
|
||||
img.src = this.poster
|
||||
}
|
||||
|
||||
async stopIterator() {
|
||||
await this.videoIterator?.return()
|
||||
this.videoIterator = null
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
}
|
||||
|
||||
async load(src, onMetadata) {
|
||||
const id = ++this.asyncId
|
||||
|
||||
await this.stopIterator()
|
||||
this.clear()
|
||||
this.posterDrawn = false
|
||||
|
||||
if (!(await this.preflight(src))) return
|
||||
|
||||
const source = this.normalizeSource(src)
|
||||
if (!source) {
|
||||
this.drawPoster()
|
||||
return
|
||||
}
|
||||
|
||||
this.input = new Input({
|
||||
source,
|
||||
formats: ALL_FORMATS,
|
||||
})
|
||||
|
||||
this.duration = await this.input.computeDuration()
|
||||
if (id !== this.asyncId) return
|
||||
|
||||
const videoTrack = await this.input.getPrimaryVideoTrack()
|
||||
if (!videoTrack) {
|
||||
this.handleNoVideoTrack()
|
||||
onMetadata?.()
|
||||
return
|
||||
}
|
||||
|
||||
if (videoTrack.codec === null || !(await videoTrack.canDecode())) {
|
||||
this.handleNoVideoTrack()
|
||||
onMetadata?.()
|
||||
return
|
||||
}
|
||||
|
||||
const transparent = await videoTrack.canBeTransparent()
|
||||
this.videoSink = new CanvasSink(videoTrack, {
|
||||
poolSize: 2,
|
||||
fit: 'contain',
|
||||
alpha: transparent,
|
||||
})
|
||||
|
||||
this.width = videoTrack.displayWidth
|
||||
this.height = videoTrack.displayHeight
|
||||
|
||||
this.canvas.width = this.width
|
||||
this.canvas.height = this.height
|
||||
|
||||
onMetadata?.()
|
||||
|
||||
await this.resetIterator(0)
|
||||
}
|
||||
|
||||
handleNoVideoTrack() {
|
||||
this.videoSink = null
|
||||
this.width = 0
|
||||
this.height = 0
|
||||
this.canvas.width = 0
|
||||
this.canvas.height = 0
|
||||
this.clear()
|
||||
this.drawPoster()
|
||||
}
|
||||
|
||||
async resetIterator(time) {
|
||||
await this.stopIterator()
|
||||
|
||||
if (!this.videoSink) return
|
||||
|
||||
this.videoIterator = this.videoSink.canvases(time)
|
||||
|
||||
const first = (await this.videoIterator.next()).value ?? null
|
||||
const second = (await this.videoIterator.next()).value ?? null
|
||||
|
||||
this.nextFrame = second
|
||||
|
||||
if (first) {
|
||||
this.ctx.drawImage(first.canvas, 0, 0)
|
||||
this.events.emit('loadeddata')
|
||||
} else {
|
||||
this.drawPoster()
|
||||
}
|
||||
}
|
||||
|
||||
async updateNextFrame(localId) {
|
||||
if (!this.videoIterator) return
|
||||
|
||||
while (true) {
|
||||
const frame = (await this.videoIterator.next()).value ?? null
|
||||
if (!frame || localId !== this.asyncId) return
|
||||
|
||||
const t = this.audioClock.currentTime
|
||||
const tolerance = this.dropLateFrames
|
||||
? Math.max(0.06, this.avSyncTolerance / Math.max(1, this.playbackRate))
|
||||
: 0
|
||||
|
||||
if (this.dropLateFrames && frame.timestamp < t - tolerance) {
|
||||
// Skip late frame
|
||||
continue
|
||||
}
|
||||
|
||||
if (frame.timestamp <= t + tolerance) {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
this.ctx.drawImage(frame.canvas, 0, 0)
|
||||
|
||||
if (!this.dropLateFrames && frame.timestamp > t) {
|
||||
this.nextFrame = null
|
||||
return
|
||||
}
|
||||
} else {
|
||||
this.nextFrame = frame
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.audioClock) return
|
||||
|
||||
const t = this.audioClock.currentTime
|
||||
const now = Date.now()
|
||||
|
||||
// Emit timeupdate event
|
||||
if (now - this.lastTimeUpdate >= this.timeupdateInterval) {
|
||||
this.events.emit('timeupdate')
|
||||
this.lastTimeUpdate = now
|
||||
}
|
||||
|
||||
// Check if reached end
|
||||
if (Number.isFinite(this.duration) && t >= this.duration) {
|
||||
this.stop()
|
||||
this.stalled = false
|
||||
this.events.emit('ended')
|
||||
this.events.emit('pause')
|
||||
this.events.emit('canplay')
|
||||
return
|
||||
}
|
||||
|
||||
// Render next frame if ready
|
||||
if (this.nextFrame && this.nextFrame.timestamp <= t) {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
this.ctx.drawImage(this.nextFrame.canvas, 0, 0)
|
||||
this.nextFrame = null
|
||||
this.updateNextFrame(this.asyncId)
|
||||
|
||||
if (this.stalled) {
|
||||
this.events.emit('canplay')
|
||||
this.events.emit('playing')
|
||||
this.stalled = false
|
||||
}
|
||||
} else if (!this.nextFrame) {
|
||||
this.updateNextFrame(this.asyncId)
|
||||
|
||||
if (!this.nextFrame && Number.isFinite(this.duration) && t < this.duration && !this.stalled) {
|
||||
this.stalled = true
|
||||
this.events.emit('waiting')
|
||||
}
|
||||
}
|
||||
|
||||
this.rafId = requestAnimationFrame(() => this.render())
|
||||
}
|
||||
|
||||
start(audioEngine) {
|
||||
this.audioClock = audioEngine
|
||||
this.asyncId++
|
||||
this.stalled = false
|
||||
this.updateNextFrame(this.asyncId)
|
||||
this.rafId = requestAnimationFrame(() => this.render())
|
||||
}
|
||||
|
||||
stop() {
|
||||
cancelAnimationFrame(this.rafId)
|
||||
}
|
||||
|
||||
async seek(time) {
|
||||
this.asyncId++
|
||||
await this.resetIterator(time)
|
||||
}
|
||||
|
||||
setPlaybackRate(rate) {
|
||||
this.playbackRate = Math.max(0.1, Number(rate) || 1)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.asyncId++
|
||||
this.stop()
|
||||
this.stopIterator()
|
||||
this.posterDrawn = false
|
||||
this.input = null
|
||||
this.videoSink = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* Video Element Shim
|
||||
* Simulates HTMLVideoElement interface for MediaBunny
|
||||
*/
|
||||
import MediaBunnyEngine from './MediaBunnyEngine.js'
|
||||
import EventTarget from './EventTarget.js'
|
||||
|
||||
function clamp(v, min, max) {
|
||||
return Math.max(min, Math.min(max, Number(v) || 0))
|
||||
}
|
||||
|
||||
export default class VideoShim {
|
||||
constructor({ art, canvas, ctx, option }) {
|
||||
this.art = art
|
||||
this.canvas = canvas
|
||||
this.option = option
|
||||
|
||||
// Event system
|
||||
this.events = new EventTarget()
|
||||
|
||||
// MediaBunny engine
|
||||
this.engine = new MediaBunnyEngine({ canvas, ctx, events: this.events, option })
|
||||
|
||||
// Internal state
|
||||
this._src = null
|
||||
this._volume = option.volume ?? 0.7
|
||||
this._muted = !!option.muted
|
||||
this._playbackRate = 1
|
||||
|
||||
// Apply initial volume
|
||||
this.engine.setVolume(this._volume, this._muted)
|
||||
|
||||
// Forward events to ArtPlayer
|
||||
this.setupEventForwarding()
|
||||
|
||||
// Auto-load source
|
||||
if (option.source) {
|
||||
this.src = option.source
|
||||
} else if (art.option?.url) {
|
||||
this.src = art.option.url
|
||||
}
|
||||
}
|
||||
|
||||
setupEventForwarding() {
|
||||
const { events: artEvents } = this.art.constructor.config
|
||||
artEvents.forEach((name) => {
|
||||
this.events.addEventListener(name, (e) => {
|
||||
this.art.emit(`video:${e.type}`, e)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Event methods
|
||||
addEventListener(type, fn) {
|
||||
this.events.addEventListener(type, fn)
|
||||
}
|
||||
|
||||
removeEventListener(type, fn) {
|
||||
this.events.removeEventListener(type, fn)
|
||||
}
|
||||
|
||||
// Source
|
||||
get src() {
|
||||
return this._src
|
||||
}
|
||||
|
||||
set src(v) {
|
||||
this._src = v
|
||||
if (v) this.engine.load(v)
|
||||
}
|
||||
|
||||
get currentSrc() {
|
||||
return this._src
|
||||
}
|
||||
|
||||
// Time
|
||||
get currentTime() {
|
||||
return this.engine.currentTime
|
||||
}
|
||||
|
||||
set currentTime(t) {
|
||||
this.engine.seek(Number(t) || 0)
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return this.engine.duration
|
||||
}
|
||||
|
||||
// Buffered/Played/Seekable
|
||||
get buffered() {
|
||||
return this.createTimeRanges(0, this.engine.duration)
|
||||
}
|
||||
|
||||
get played() {
|
||||
return this.createTimeRanges(0, this.engine.currentTime)
|
||||
}
|
||||
|
||||
get seekable() {
|
||||
return this.createTimeRanges(0, this.engine.duration)
|
||||
}
|
||||
|
||||
createTimeRanges(start, end) {
|
||||
const duration = this.engine.duration
|
||||
if (!duration || Number.isNaN(duration) || end <= 0) {
|
||||
return { length: 0, start: () => 0, end: () => 0 }
|
||||
}
|
||||
return {
|
||||
length: 1,
|
||||
start: () => start,
|
||||
end: () => end,
|
||||
}
|
||||
}
|
||||
|
||||
// Playback state
|
||||
get paused() {
|
||||
return this.engine.paused
|
||||
}
|
||||
|
||||
get playing() {
|
||||
return !this.engine.paused && !this.engine.ended
|
||||
}
|
||||
|
||||
get ended() {
|
||||
return this.engine.ended
|
||||
}
|
||||
|
||||
get seeking() {
|
||||
return this.engine.seeking
|
||||
}
|
||||
|
||||
// Ready state
|
||||
get readyState() {
|
||||
return this.engine.readyState
|
||||
}
|
||||
|
||||
get networkState() {
|
||||
return this.engine.networkState
|
||||
}
|
||||
|
||||
get error() {
|
||||
return this.engine.error
|
||||
}
|
||||
|
||||
// Playback rate
|
||||
get playbackRate() {
|
||||
return this._playbackRate
|
||||
}
|
||||
|
||||
set playbackRate(v) {
|
||||
const rate = Number(v)
|
||||
if (Number.isNaN(rate) || rate <= 0) return
|
||||
|
||||
this._playbackRate = rate
|
||||
this.engine.setPlaybackRate(rate)
|
||||
this.events.emit('ratechange')
|
||||
}
|
||||
|
||||
// Volume
|
||||
get volume() {
|
||||
return this._volume
|
||||
}
|
||||
|
||||
set volume(v) {
|
||||
this._volume = clamp(v, 0, 1)
|
||||
this._muted = false
|
||||
this.engine.setVolume(this._volume, this._muted)
|
||||
this.events.emit('volumechange')
|
||||
}
|
||||
|
||||
get muted() {
|
||||
return this._muted
|
||||
}
|
||||
|
||||
set muted(v) {
|
||||
this._muted = !!v
|
||||
this.engine.setVolume(this._volume, this._muted)
|
||||
this.events.emit('volumechange')
|
||||
}
|
||||
|
||||
// Playback methods
|
||||
play() {
|
||||
return this.engine.play()
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.engine.pause()
|
||||
}
|
||||
|
||||
load() {
|
||||
if (this._src) this.engine.load(this._src)
|
||||
}
|
||||
|
||||
// Video dimensions
|
||||
get videoWidth() {
|
||||
return this.engine.videoWidth
|
||||
}
|
||||
|
||||
get videoHeight() {
|
||||
return this.engine.videoHeight
|
||||
}
|
||||
|
||||
// Other properties
|
||||
get poster() {
|
||||
return this.option.poster || ''
|
||||
}
|
||||
|
||||
set poster(v) {
|
||||
this.option.poster = v
|
||||
}
|
||||
|
||||
get autoplay() {
|
||||
return this.option.autoplay || false
|
||||
}
|
||||
|
||||
set autoplay(v) {}
|
||||
|
||||
get loop() {
|
||||
return this.option.loop || false
|
||||
}
|
||||
|
||||
set loop(v) {}
|
||||
|
||||
get controls() {
|
||||
return false
|
||||
}
|
||||
|
||||
set controls(v) {}
|
||||
|
||||
get playsInline() {
|
||||
return true
|
||||
}
|
||||
|
||||
set playsInline(v) {}
|
||||
|
||||
get crossOrigin() {
|
||||
return this.option.crossOrigin || ''
|
||||
}
|
||||
|
||||
set crossOrigin(v) {}
|
||||
|
||||
get preload() {
|
||||
return 'auto'
|
||||
}
|
||||
|
||||
set preload(v) {}
|
||||
|
||||
get defaultMuted() {
|
||||
return false
|
||||
}
|
||||
|
||||
set defaultMuted(v) {}
|
||||
|
||||
get defaultPlaybackRate() {
|
||||
return 1
|
||||
}
|
||||
|
||||
set defaultPlaybackRate(v) {}
|
||||
|
||||
// Methods
|
||||
canPlayType(_type) {
|
||||
return 'maybe'
|
||||
}
|
||||
|
||||
getBoundingClientRect() {
|
||||
return this.canvas.getBoundingClientRect()
|
||||
}
|
||||
|
||||
requestVideoFrameCallback(callback) {
|
||||
const id = requestAnimationFrame((time) => {
|
||||
callback(time, {
|
||||
presentationTime: this.engine.currentTime,
|
||||
expectedDisplayTime: time + 16.6,
|
||||
width: this.engine.videoWidth,
|
||||
height: this.engine.videoHeight,
|
||||
mediaTime: this.engine.currentTime,
|
||||
presentedFrames: 0,
|
||||
processingDuration: 0,
|
||||
captureTime: time,
|
||||
receiveTime: time,
|
||||
rtpTimestamp: 0,
|
||||
})
|
||||
})
|
||||
return id
|
||||
}
|
||||
|
||||
cancelVideoFrameCallback(id) {
|
||||
cancelAnimationFrame(id)
|
||||
}
|
||||
|
||||
setAttribute(name, value) {
|
||||
if (name === 'src') {
|
||||
this.src = value
|
||||
} else if (name === 'autoplay') {
|
||||
this.autoplay = value
|
||||
} else if (name === 'loop') {
|
||||
this.loop = value
|
||||
} else if (name === 'muted') {
|
||||
this.muted = true
|
||||
} else {
|
||||
this.canvas.setAttribute(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.engine.destroy()
|
||||
}
|
||||
}
|
||||
@@ -1,316 +0,0 @@
|
||||
import {
|
||||
ALL_FORMATS,
|
||||
AudioBufferSink,
|
||||
BlobSource,
|
||||
Input,
|
||||
ReadableStreamSource,
|
||||
UrlSource,
|
||||
} from 'mediabunny'
|
||||
|
||||
export default function createAudioEngine(events) {
|
||||
let input = null
|
||||
let audioSink = null
|
||||
|
||||
let audioContext = null
|
||||
let gainNode = null
|
||||
|
||||
let audioIterator = null
|
||||
let audioContextStartTime = 0
|
||||
let playbackTimeAtStart = 0
|
||||
let latestScheduledEndTime = 0
|
||||
|
||||
let _duration = Number.NaN
|
||||
let _paused = true
|
||||
|
||||
let _volume = 0.7
|
||||
let _muted = false
|
||||
let _playbackRate = 1
|
||||
|
||||
let asyncId = 0
|
||||
const queuedNodes = new Set()
|
||||
|
||||
function getCurrentTime() {
|
||||
if (_paused)
|
||||
return playbackTimeAtStart
|
||||
return (
|
||||
(audioContext.currentTime - audioContextStartTime) * _playbackRate
|
||||
+ playbackTimeAtStart
|
||||
)
|
||||
}
|
||||
|
||||
function updateGain() {
|
||||
if (!gainNode)
|
||||
return
|
||||
const v = _muted ? 0 : _volume
|
||||
gainNode.gain.value = v * v
|
||||
}
|
||||
|
||||
function ensureAudioContext(sampleRate) {
|
||||
if (audioContext)
|
||||
return
|
||||
|
||||
const AudioContext
|
||||
= window.AudioContext || window.webkitAudioContext
|
||||
|
||||
try {
|
||||
audioContext = new AudioContext({ sampleRate })
|
||||
}
|
||||
catch {
|
||||
audioContext = new AudioContext()
|
||||
}
|
||||
|
||||
gainNode = audioContext.createGain()
|
||||
gainNode.connect(audioContext.destination)
|
||||
updateGain()
|
||||
}
|
||||
|
||||
function stopQueuedNodes() {
|
||||
for (const node of queuedNodes) {
|
||||
node.stop()
|
||||
}
|
||||
queuedNodes.clear()
|
||||
}
|
||||
|
||||
async function stopIterator() {
|
||||
await audioIterator?.return()
|
||||
audioIterator = null
|
||||
}
|
||||
|
||||
function normalizeSource(src) {
|
||||
if (typeof src === 'string')
|
||||
return new UrlSource(src)
|
||||
if (src instanceof Blob)
|
||||
return new BlobSource(src)
|
||||
if (typeof ReadableStream !== 'undefined' && src instanceof ReadableStream)
|
||||
return new ReadableStreamSource(src)
|
||||
return src
|
||||
}
|
||||
|
||||
async function load(src, onMetadata) {
|
||||
asyncId++
|
||||
const id = asyncId
|
||||
|
||||
await stopIterator()
|
||||
stopQueuedNodes()
|
||||
|
||||
_paused = true
|
||||
playbackTimeAtStart = 0
|
||||
audioContextStartTime = 0
|
||||
|
||||
const source = normalizeSource(src)
|
||||
if (!source)
|
||||
return
|
||||
|
||||
input = new Input({
|
||||
source,
|
||||
formats: ALL_FORMATS,
|
||||
})
|
||||
|
||||
_duration = await input.computeDuration()
|
||||
if (id !== asyncId)
|
||||
return
|
||||
|
||||
const audioTrack = await input.getPrimaryAudioTrack()
|
||||
if (!audioTrack) {
|
||||
audioSink = null
|
||||
ensureAudioContext()
|
||||
if (onMetadata)
|
||||
onMetadata()
|
||||
return
|
||||
}
|
||||
|
||||
if (audioTrack.codec === null || !(await audioTrack.canDecode())) {
|
||||
audioSink = null
|
||||
ensureAudioContext()
|
||||
if (onMetadata)
|
||||
onMetadata()
|
||||
return
|
||||
}
|
||||
|
||||
ensureAudioContext(audioTrack.sampleRate)
|
||||
audioSink = new AudioBufferSink(audioTrack)
|
||||
|
||||
if (onMetadata)
|
||||
onMetadata()
|
||||
}
|
||||
|
||||
async function runIterator(localId) {
|
||||
if (!audioSink)
|
||||
return
|
||||
|
||||
await stopIterator()
|
||||
audioIterator = audioSink.buffers(getCurrentTime())
|
||||
|
||||
while (true) {
|
||||
if (localId !== asyncId)
|
||||
return
|
||||
if (_paused)
|
||||
return
|
||||
|
||||
const nextPromise = audioIterator.next()
|
||||
|
||||
const checkStarvation = setInterval(() => {
|
||||
if (localId !== asyncId || _paused) {
|
||||
clearInterval(checkStarvation)
|
||||
return
|
||||
}
|
||||
if (audioContext.state === 'running'
|
||||
&& audioContext.currentTime >= latestScheduledEndTime - 0.2) {
|
||||
audioContext.suspend()
|
||||
events.emit('waiting')
|
||||
}
|
||||
}, 50)
|
||||
|
||||
let result
|
||||
try {
|
||||
result = await nextPromise
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
break
|
||||
}
|
||||
finally {
|
||||
clearInterval(checkStarvation)
|
||||
}
|
||||
|
||||
if (localId !== asyncId)
|
||||
return
|
||||
if (_paused)
|
||||
return
|
||||
|
||||
if (audioContext.state === 'suspended') {
|
||||
await audioContext.resume()
|
||||
events.emit('canplay')
|
||||
events.emit('playing')
|
||||
}
|
||||
|
||||
if (result.done)
|
||||
break
|
||||
|
||||
const { buffer, timestamp } = result.value
|
||||
|
||||
const node = audioContext.createBufferSource()
|
||||
node.buffer = buffer
|
||||
node.connect(gainNode)
|
||||
node.playbackRate.value = _playbackRate
|
||||
|
||||
const startAt
|
||||
= audioContextStartTime
|
||||
+ (timestamp - playbackTimeAtStart) / _playbackRate
|
||||
|
||||
const duration = buffer.duration
|
||||
const endAt = startAt + duration / _playbackRate
|
||||
|
||||
if (endAt > latestScheduledEndTime) {
|
||||
latestScheduledEndTime = endAt
|
||||
}
|
||||
|
||||
if (startAt >= audioContext.currentTime) {
|
||||
node.start(startAt)
|
||||
}
|
||||
else {
|
||||
node.start(
|
||||
audioContext.currentTime,
|
||||
(audioContext.currentTime - startAt) * _playbackRate,
|
||||
)
|
||||
}
|
||||
|
||||
queuedNodes.add(node)
|
||||
node.onended = () => queuedNodes.delete(node)
|
||||
}
|
||||
}
|
||||
|
||||
async function play() {
|
||||
if (!_paused)
|
||||
return
|
||||
|
||||
if (!audioContext) {
|
||||
ensureAudioContext()
|
||||
}
|
||||
|
||||
if (audioContext.state === 'suspended') {
|
||||
await audioContext.resume()
|
||||
}
|
||||
|
||||
audioContextStartTime = audioContext.currentTime
|
||||
latestScheduledEndTime = audioContextStartTime
|
||||
_paused = false
|
||||
|
||||
asyncId++
|
||||
runIterator(asyncId)
|
||||
}
|
||||
|
||||
function pause() {
|
||||
if (_paused)
|
||||
return
|
||||
|
||||
playbackTimeAtStart = getCurrentTime()
|
||||
_paused = true
|
||||
|
||||
stopIterator()
|
||||
stopQueuedNodes()
|
||||
}
|
||||
|
||||
async function seek(time) {
|
||||
playbackTimeAtStart = Math.max(0, time)
|
||||
audioContextStartTime = audioContext.currentTime
|
||||
latestScheduledEndTime = audioContextStartTime
|
||||
|
||||
asyncId++
|
||||
if (!_paused) {
|
||||
runIterator(asyncId)
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
asyncId++
|
||||
pause()
|
||||
audioContext?.close()
|
||||
audioContext = null
|
||||
input = null
|
||||
audioSink = null
|
||||
}
|
||||
|
||||
return {
|
||||
load,
|
||||
play,
|
||||
pause,
|
||||
seek,
|
||||
destroy,
|
||||
|
||||
setVolume(v, muted) {
|
||||
_volume = v
|
||||
_muted = muted
|
||||
updateGain()
|
||||
},
|
||||
|
||||
setPlaybackRate(rate) {
|
||||
if (rate === _playbackRate)
|
||||
return
|
||||
|
||||
if (!_paused) {
|
||||
playbackTimeAtStart = getCurrentTime()
|
||||
audioContextStartTime = audioContext.currentTime
|
||||
}
|
||||
|
||||
_playbackRate = rate
|
||||
|
||||
if (!_paused) {
|
||||
asyncId++
|
||||
runIterator(asyncId)
|
||||
}
|
||||
},
|
||||
|
||||
get currentTime() {
|
||||
return getCurrentTime()
|
||||
},
|
||||
|
||||
get duration() {
|
||||
return _duration
|
||||
},
|
||||
|
||||
get paused() {
|
||||
return _paused
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
import createAudioEngine from './audioEngine.js'
|
||||
import createVideoEngine from './videoEngine.js'
|
||||
|
||||
export default function createMediabunnyEngine({
|
||||
canvas,
|
||||
ctx,
|
||||
events,
|
||||
option = {},
|
||||
}) {
|
||||
const defaults = {
|
||||
timeupdateInterval: 250,
|
||||
avSyncTolerance: 0.12,
|
||||
loadTimeout: 12000,
|
||||
dropLateFrames: false,
|
||||
poster: '',
|
||||
}
|
||||
|
||||
const audio = createAudioEngine(events)
|
||||
const video = createVideoEngine({
|
||||
canvas,
|
||||
ctx,
|
||||
events,
|
||||
timeupdateInterval: option.timeupdateInterval ?? defaults.timeupdateInterval,
|
||||
avSyncTolerance: option.avSyncTolerance ?? defaults.avSyncTolerance,
|
||||
dropLateFrames: option.dropLateFrames ?? defaults.dropLateFrames,
|
||||
poster: option.poster ?? defaults.poster,
|
||||
preflightRange: option.preflightRange ?? false,
|
||||
})
|
||||
|
||||
let _paused = true
|
||||
let _ended = false
|
||||
let _readyState = 0
|
||||
let _networkState = 0
|
||||
let _error = null
|
||||
let _seeking = false
|
||||
let loadSeq = 0
|
||||
|
||||
events.addEventListener?.('ended', () => {
|
||||
_ended = true
|
||||
_paused = true
|
||||
})
|
||||
|
||||
async function load(src) {
|
||||
const id = ++loadSeq
|
||||
pause()
|
||||
_ended = false
|
||||
_error = null
|
||||
_networkState = 2
|
||||
_readyState = 0
|
||||
setTimeout(() => events.emit('waiting'), 0)
|
||||
setTimeout(() => events.emit('loadstart'), 0)
|
||||
|
||||
const loadTimeout = Number.isFinite(option.loadTimeout)
|
||||
? option.loadTimeout
|
||||
: defaults.loadTimeout
|
||||
|
||||
let timer = null
|
||||
const timeoutPromise
|
||||
= loadTimeout > 0
|
||||
? new Promise((_, reject) => {
|
||||
timer = setTimeout(() => reject(new Error('Load timeout')), loadTimeout)
|
||||
})
|
||||
: null
|
||||
|
||||
const loadTask = (async () => {
|
||||
try {
|
||||
let videoMetadataLoaded = false
|
||||
let audioMetadataLoaded = false
|
||||
|
||||
const checkMetadata = () => {
|
||||
if (videoMetadataLoaded && audioMetadataLoaded) {
|
||||
_readyState = 1
|
||||
events.emit('loadedmetadata')
|
||||
events.emit('durationchange')
|
||||
}
|
||||
}
|
||||
|
||||
const p1 = video.load(src, () => {
|
||||
if (id !== loadSeq)
|
||||
return
|
||||
videoMetadataLoaded = true
|
||||
checkMetadata()
|
||||
})
|
||||
|
||||
const p2 = audio.load(src, () => {
|
||||
if (id !== loadSeq)
|
||||
return
|
||||
audioMetadataLoaded = true
|
||||
checkMetadata()
|
||||
})
|
||||
|
||||
await Promise.all([p1, p2])
|
||||
if (id !== loadSeq)
|
||||
return
|
||||
|
||||
_readyState = 4
|
||||
_networkState = 1
|
||||
events.emit('loadeddata')
|
||||
events.emit('canplay')
|
||||
events.emit('canplaythrough')
|
||||
}
|
||||
catch (err) {
|
||||
if (id !== loadSeq)
|
||||
return
|
||||
_error = { code: 4, message: err.message }
|
||||
_networkState = 3
|
||||
events.emit('error')
|
||||
console.error(err)
|
||||
}
|
||||
finally {
|
||||
if (timer)
|
||||
clearTimeout(timer)
|
||||
}
|
||||
})()
|
||||
|
||||
if (timeoutPromise) {
|
||||
try {
|
||||
await Promise.race([loadTask, timeoutPromise])
|
||||
}
|
||||
catch (err) {
|
||||
if (id !== loadSeq)
|
||||
return
|
||||
loadSeq++
|
||||
_error = { code: 4, message: err.message }
|
||||
_networkState = 3
|
||||
events.emit('error')
|
||||
}
|
||||
}
|
||||
else {
|
||||
await loadTask
|
||||
}
|
||||
}
|
||||
|
||||
async function play() {
|
||||
if (!_paused)
|
||||
return
|
||||
if (_ended) {
|
||||
_ended = false
|
||||
await seek(0)
|
||||
}
|
||||
_paused = false
|
||||
await audio.play()
|
||||
video.start(audio)
|
||||
events.emit('play')
|
||||
events.emit('playing')
|
||||
}
|
||||
|
||||
function pause() {
|
||||
if (_paused)
|
||||
return
|
||||
_paused = true
|
||||
audio.pause()
|
||||
video.stop()
|
||||
events.emit('pause')
|
||||
}
|
||||
|
||||
async function seek(time) {
|
||||
const shouldResume = !_paused
|
||||
_ended = false
|
||||
_seeking = true
|
||||
events.emit('seeking')
|
||||
events.emit('waiting')
|
||||
pause()
|
||||
await audio.seek(time)
|
||||
await video.seek(time)
|
||||
_seeking = false
|
||||
events.emit('seeked')
|
||||
if (shouldResume && !_ended) {
|
||||
await play()
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
pause()
|
||||
audio.destroy()
|
||||
video.destroy()
|
||||
}
|
||||
|
||||
return {
|
||||
load,
|
||||
play,
|
||||
pause,
|
||||
seek,
|
||||
destroy,
|
||||
|
||||
setVolume: audio.setVolume,
|
||||
setPlaybackRate(rate) {
|
||||
audio.setPlaybackRate(rate)
|
||||
video.setPlaybackRate(rate)
|
||||
},
|
||||
|
||||
get currentTime() {
|
||||
return audio.currentTime
|
||||
},
|
||||
get duration() {
|
||||
return audio.duration || video.duration
|
||||
},
|
||||
get paused() {
|
||||
return _paused
|
||||
},
|
||||
get seeking() {
|
||||
return _seeking
|
||||
},
|
||||
get ended() {
|
||||
return _ended
|
||||
},
|
||||
get readyState() {
|
||||
return _readyState
|
||||
},
|
||||
get networkState() {
|
||||
return _networkState
|
||||
},
|
||||
get error() {
|
||||
return _error
|
||||
},
|
||||
get videoWidth() {
|
||||
return video.width
|
||||
},
|
||||
get videoHeight() {
|
||||
return video.height
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
import {
|
||||
ALL_FORMATS,
|
||||
BlobSource,
|
||||
CanvasSink,
|
||||
Input,
|
||||
ReadableStreamSource,
|
||||
UrlSource,
|
||||
} from 'mediabunny'
|
||||
|
||||
export default function createVideoEngine({
|
||||
canvas,
|
||||
ctx,
|
||||
events,
|
||||
timeupdateInterval = 250,
|
||||
avSyncTolerance = 0.12,
|
||||
dropLateFrames = false,
|
||||
poster = '',
|
||||
preflightRange = false,
|
||||
}) {
|
||||
let input = null
|
||||
let videoSink = null
|
||||
let videoIterator = null
|
||||
|
||||
let nextFrame = null
|
||||
let rafId = 0
|
||||
let asyncId = 0
|
||||
|
||||
let _width = 0
|
||||
let _height = 0
|
||||
let _duration = Number.NaN
|
||||
|
||||
let audioClock = null
|
||||
let lastTimeUpdate = 0
|
||||
let stalled = false
|
||||
let playbackRate = 1
|
||||
let posterDrawn = false
|
||||
|
||||
function normalizeSource(src) {
|
||||
if (typeof src === 'string')
|
||||
return new UrlSource(src)
|
||||
if (src instanceof Blob)
|
||||
return new BlobSource(src)
|
||||
if (typeof ReadableStream !== 'undefined' && src instanceof ReadableStream)
|
||||
return new ReadableStreamSource(src)
|
||||
return src
|
||||
}
|
||||
|
||||
async function preflight(url) {
|
||||
if (!preflightRange || typeof url !== 'string')
|
||||
return true
|
||||
try {
|
||||
const res = await fetch(url, { method: 'HEAD' })
|
||||
const acceptRanges = res.headers.get('accept-ranges')
|
||||
if (!acceptRanges || acceptRanges === 'none') {
|
||||
events.emit('error', new Event('RangeNotSupported'))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('preflight failed', e)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function drawPoster() {
|
||||
if (!poster || posterDrawn)
|
||||
return
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
canvas.width = img.naturalWidth || canvas.width
|
||||
canvas.height = img.naturalHeight || canvas.height
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
||||
posterDrawn = true
|
||||
}
|
||||
img.src = poster
|
||||
}
|
||||
|
||||
async function stopIterator() {
|
||||
await videoIterator?.return()
|
||||
videoIterator = null
|
||||
}
|
||||
|
||||
function clear() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
|
||||
async function load(src, onMetadata) {
|
||||
asyncId++
|
||||
const id = asyncId
|
||||
|
||||
await stopIterator()
|
||||
clear()
|
||||
posterDrawn = false
|
||||
|
||||
if (!(await preflight(src)))
|
||||
return
|
||||
|
||||
const source = normalizeSource(src)
|
||||
if (!source) {
|
||||
drawPoster()
|
||||
return
|
||||
}
|
||||
|
||||
input = new Input({
|
||||
source,
|
||||
formats: ALL_FORMATS,
|
||||
})
|
||||
|
||||
_duration = await input.computeDuration()
|
||||
if (id !== asyncId)
|
||||
return
|
||||
|
||||
const videoTrack = await input.getPrimaryVideoTrack()
|
||||
if (!videoTrack) {
|
||||
videoSink = null
|
||||
_width = 0
|
||||
_height = 0
|
||||
canvas.width = 0
|
||||
canvas.height = 0
|
||||
drawPoster()
|
||||
if (onMetadata)
|
||||
onMetadata()
|
||||
return
|
||||
}
|
||||
|
||||
if (videoTrack.codec === null || !(await videoTrack.canDecode())) {
|
||||
videoSink = null
|
||||
_width = 0
|
||||
_height = 0
|
||||
canvas.width = 0
|
||||
canvas.height = 0
|
||||
clear()
|
||||
drawPoster()
|
||||
if (onMetadata)
|
||||
onMetadata()
|
||||
return
|
||||
}
|
||||
|
||||
const transparent = await videoTrack.canBeTransparent()
|
||||
videoSink = new CanvasSink(videoTrack, {
|
||||
poolSize: 2,
|
||||
fit: 'contain',
|
||||
alpha: transparent,
|
||||
})
|
||||
|
||||
_width = videoTrack.displayWidth
|
||||
_height = videoTrack.displayHeight
|
||||
|
||||
canvas.width = _width
|
||||
canvas.height = _height
|
||||
|
||||
if (onMetadata)
|
||||
onMetadata()
|
||||
|
||||
await resetIterator(0)
|
||||
}
|
||||
|
||||
async function resetIterator(time) {
|
||||
await stopIterator()
|
||||
|
||||
if (!videoSink)
|
||||
return
|
||||
|
||||
videoIterator = videoSink.canvases(time)
|
||||
|
||||
const first = (await videoIterator.next()).value ?? null
|
||||
const second = (await videoIterator.next()).value ?? null
|
||||
|
||||
nextFrame = second
|
||||
|
||||
if (first) {
|
||||
ctx.drawImage(first.canvas, 0, 0)
|
||||
events.emit('loadeddata')
|
||||
}
|
||||
else {
|
||||
drawPoster()
|
||||
}
|
||||
}
|
||||
|
||||
async function updateNextFrame(localId) {
|
||||
if (!videoIterator)
|
||||
return
|
||||
|
||||
while (true) {
|
||||
const frame = (await videoIterator.next()).value ?? null
|
||||
if (!frame)
|
||||
return
|
||||
if (localId !== asyncId)
|
||||
return
|
||||
|
||||
const t = audioClock.currentTime
|
||||
if (dropLateFrames) {
|
||||
const tolerance = Math.max(0.06, avSyncTolerance / Math.max(1, playbackRate))
|
||||
if (frame.timestamp < t - tolerance) {
|
||||
continue
|
||||
}
|
||||
if (frame.timestamp <= t + tolerance) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
ctx.drawImage(frame.canvas, 0, 0)
|
||||
}
|
||||
else {
|
||||
nextFrame = frame
|
||||
return
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (frame.timestamp <= t) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
ctx.drawImage(frame.canvas, 0, 0)
|
||||
}
|
||||
else {
|
||||
nextFrame = frame
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!audioClock)
|
||||
return
|
||||
|
||||
const t = audioClock.currentTime
|
||||
const now = Date.now()
|
||||
if (now - lastTimeUpdate >= timeupdateInterval) {
|
||||
events.emit('timeupdate')
|
||||
lastTimeUpdate = now
|
||||
}
|
||||
|
||||
if (Number.isFinite(_duration) && t >= _duration) {
|
||||
stop()
|
||||
stalled = false
|
||||
events.emit('ended')
|
||||
events.emit('pause')
|
||||
events.emit('canplay')
|
||||
return
|
||||
}
|
||||
|
||||
if (nextFrame && nextFrame.timestamp <= t) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
ctx.drawImage(nextFrame.canvas, 0, 0)
|
||||
nextFrame = null
|
||||
updateNextFrame(asyncId)
|
||||
if (stalled) {
|
||||
events.emit('canplay')
|
||||
events.emit('playing')
|
||||
stalled = false
|
||||
}
|
||||
}
|
||||
else if (!nextFrame) {
|
||||
updateNextFrame(asyncId)
|
||||
if (!nextFrame && Number.isFinite(_duration) && t < _duration && !stalled) {
|
||||
stalled = true
|
||||
events.emit('waiting')
|
||||
}
|
||||
}
|
||||
|
||||
rafId = requestAnimationFrame(render)
|
||||
}
|
||||
|
||||
function start(audioEngine) {
|
||||
audioClock = audioEngine
|
||||
asyncId++
|
||||
stalled = false
|
||||
updateNextFrame(asyncId)
|
||||
rafId = requestAnimationFrame(render)
|
||||
}
|
||||
|
||||
function stop() {
|
||||
cancelAnimationFrame(rafId)
|
||||
}
|
||||
|
||||
async function seek(time) {
|
||||
asyncId++
|
||||
await resetIterator(time)
|
||||
}
|
||||
|
||||
function setPlaybackRate(rate) {
|
||||
playbackRate = Math.max(0.1, Number(rate) || 1)
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
asyncId++
|
||||
stop()
|
||||
stopIterator()
|
||||
posterDrawn = false
|
||||
input = null
|
||||
videoSink = null
|
||||
}
|
||||
|
||||
return {
|
||||
load,
|
||||
start,
|
||||
stop,
|
||||
seek,
|
||||
setPlaybackRate,
|
||||
destroy,
|
||||
|
||||
get width() {
|
||||
return _width
|
||||
},
|
||||
get height() {
|
||||
return _height
|
||||
},
|
||||
get duration() {
|
||||
return _duration
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,84 @@
|
||||
import createCanvasProxy from './proxy/canvasProxy.js'
|
||||
/**
|
||||
* ArtPlayer MediaBunny Proxy
|
||||
* Main entry point
|
||||
*/
|
||||
import VideoShim from './VideoShim.js'
|
||||
|
||||
export default function artplayerProxyMediabunny(option = {}) {
|
||||
return (art) => {
|
||||
return createCanvasProxy(art, option)
|
||||
const { constructor } = art
|
||||
const { createElement } = constructor.utils
|
||||
|
||||
// Create canvas element
|
||||
const canvas = createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
// Create video shim
|
||||
const shim = new VideoShim({
|
||||
art,
|
||||
canvas,
|
||||
ctx,
|
||||
option,
|
||||
})
|
||||
|
||||
// Proxy canvas methods to shim
|
||||
const originalCanvasMethods = {}
|
||||
for (const prop in canvas) {
|
||||
if (typeof canvas[prop] === 'function') {
|
||||
originalCanvasMethods[prop] = canvas[prop].bind(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
// Get all properties from shim instance and prototype
|
||||
const propertyNames = new Set([
|
||||
...Object.getOwnPropertyNames(shim),
|
||||
...Object.getOwnPropertyNames(Object.getPrototypeOf(shim)),
|
||||
])
|
||||
|
||||
// Add shim properties to canvas
|
||||
for (const prop of propertyNames) {
|
||||
if (prop === 'constructor') continue
|
||||
if (!(prop in canvas)) {
|
||||
Object.defineProperty(canvas, prop, {
|
||||
get() {
|
||||
const value = shim[prop]
|
||||
return typeof value === 'function' ? value.bind(shim) : value
|
||||
},
|
||||
set(v) {
|
||||
shim[prop] = v
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Restore original canvas methods
|
||||
for (const prop in originalCanvasMethods) {
|
||||
canvas[prop] = (...args) => originalCanvasMethods[prop](...args)
|
||||
}
|
||||
|
||||
// Handle resize
|
||||
function resize() {
|
||||
const player = art.template?.$player
|
||||
if (!player || art.option.autoSize) return
|
||||
|
||||
Object.assign(canvas.style, {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
})
|
||||
}
|
||||
|
||||
art.on('resize', resize)
|
||||
art.on('video:loadedmetadata', resize)
|
||||
|
||||
// Cleanup on destroy
|
||||
art.on('destroy', () => {
|
||||
shim.destroy()
|
||||
})
|
||||
|
||||
return canvas
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import createMediabunnyVideoShim from '../shim/mediabunnyVideoShim.js'
|
||||
|
||||
export default function createCanvasProxy(art, option) {
|
||||
const { constructor } = art
|
||||
const { createElement } = constructor.utils
|
||||
|
||||
const canvas = createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
const shim = createMediabunnyVideoShim({
|
||||
art,
|
||||
canvas,
|
||||
ctx,
|
||||
option,
|
||||
})
|
||||
|
||||
const originalCanvasMethods = {}
|
||||
for (const prop in canvas) {
|
||||
if (typeof canvas[prop] === 'function') {
|
||||
originalCanvasMethods[prop] = canvas[prop].bind(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
for (const prop in shim) {
|
||||
if (!(prop in canvas)) {
|
||||
Object.defineProperty(canvas, prop, {
|
||||
get() {
|
||||
const value = shim[prop]
|
||||
return typeof value === 'function' ? value.bind(shim) : value
|
||||
},
|
||||
set(v) {
|
||||
shim[prop] = v
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const prop in originalCanvasMethods) {
|
||||
canvas[prop] = (...args) => originalCanvasMethods[prop](...args)
|
||||
}
|
||||
|
||||
art.on('destroy', () => {
|
||||
shim.destroy()
|
||||
})
|
||||
|
||||
function resize() {
|
||||
const player = art.template?.$player
|
||||
if (!player || art.option.autoSize)
|
||||
return
|
||||
|
||||
Object.assign(canvas.style, {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
})
|
||||
}
|
||||
|
||||
art.on('resize', resize)
|
||||
art.on('video:loadedmetadata', resize)
|
||||
|
||||
return canvas
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
import createMediabunnyEngine from '../engine/mediabunnyEngine.js'
|
||||
import clamp from '../utils/clamp.js'
|
||||
import { createVideoEventTarget } from './videoEvents.js'
|
||||
|
||||
export default function createMediabunnyVideoShim({ art, canvas, ctx, option }) {
|
||||
const events = createVideoEventTarget()
|
||||
const engine = createMediabunnyEngine({ canvas, ctx, events, option })
|
||||
|
||||
let _src = null
|
||||
let _volume = option.volume ?? 0.7
|
||||
let _muted = !!option.muted
|
||||
let _playbackRate = 1
|
||||
|
||||
engine.setVolume(_volume, _muted)
|
||||
|
||||
const { events: artEvents } = art.constructor.config
|
||||
artEvents.forEach((name) => {
|
||||
events.addEventListener(name, (e) => {
|
||||
art.emit(`video:${e.type}`, e)
|
||||
})
|
||||
})
|
||||
|
||||
const shim = {
|
||||
addEventListener: events.addEventListener,
|
||||
removeEventListener: events.removeEventListener,
|
||||
|
||||
get src() {
|
||||
return _src
|
||||
},
|
||||
set src(v) {
|
||||
_src = v
|
||||
if (v)
|
||||
engine.load(v)
|
||||
},
|
||||
|
||||
get currentTime() {
|
||||
return engine.currentTime
|
||||
},
|
||||
set currentTime(t) {
|
||||
engine.seek(Number(t) || 0)
|
||||
},
|
||||
|
||||
get duration() {
|
||||
return engine.duration
|
||||
},
|
||||
|
||||
get buffered() {
|
||||
const duration = engine.duration
|
||||
if (!duration || Number.isNaN(duration)) {
|
||||
return {
|
||||
length: 0,
|
||||
start: () => 0,
|
||||
end: () => 0,
|
||||
}
|
||||
}
|
||||
return {
|
||||
length: 1,
|
||||
start: () => 0,
|
||||
end: () => duration,
|
||||
}
|
||||
},
|
||||
|
||||
get played() {
|
||||
const duration = engine.duration
|
||||
const currentTime = engine.currentTime
|
||||
if (!duration || Number.isNaN(duration) || currentTime <= 0) {
|
||||
return {
|
||||
length: 0,
|
||||
start: () => 0,
|
||||
end: () => 0,
|
||||
}
|
||||
}
|
||||
return {
|
||||
length: 1,
|
||||
start: () => 0,
|
||||
end: () => currentTime,
|
||||
}
|
||||
},
|
||||
|
||||
get paused() {
|
||||
return engine.paused
|
||||
},
|
||||
get playing() {
|
||||
return !engine.paused && !engine.ended
|
||||
},
|
||||
get ended() {
|
||||
return engine.ended
|
||||
},
|
||||
|
||||
get readyState() {
|
||||
return engine.readyState
|
||||
},
|
||||
|
||||
get networkState() {
|
||||
return engine.networkState
|
||||
},
|
||||
|
||||
get error() {
|
||||
return engine.error
|
||||
},
|
||||
|
||||
get seeking() {
|
||||
return engine.seeking
|
||||
},
|
||||
|
||||
get seekable() {
|
||||
const duration = engine.duration
|
||||
if (!duration || Number.isNaN(duration)) {
|
||||
return {
|
||||
length: 0,
|
||||
start: () => 0,
|
||||
end: () => 0,
|
||||
}
|
||||
}
|
||||
return {
|
||||
length: 1,
|
||||
start: () => 0,
|
||||
end: () => duration,
|
||||
}
|
||||
},
|
||||
|
||||
get playbackRate() {
|
||||
return _playbackRate
|
||||
},
|
||||
set playbackRate(v) {
|
||||
const rate = Number(v)
|
||||
if (Number.isNaN(rate) || rate <= 0)
|
||||
return
|
||||
_playbackRate = rate
|
||||
engine.setPlaybackRate(rate)
|
||||
events.emit('ratechange')
|
||||
},
|
||||
|
||||
get volume() {
|
||||
return _volume
|
||||
},
|
||||
set volume(v) {
|
||||
_volume = clamp(v, 0, 1)
|
||||
_muted = false
|
||||
engine.setVolume(_volume, _muted)
|
||||
events.emit('volumechange')
|
||||
},
|
||||
|
||||
get muted() {
|
||||
return _muted
|
||||
},
|
||||
set muted(v) {
|
||||
_muted = !!v
|
||||
engine.setVolume(_volume, _muted)
|
||||
events.emit('volumechange')
|
||||
},
|
||||
|
||||
play() {
|
||||
return engine.play()
|
||||
},
|
||||
pause() {
|
||||
engine.pause()
|
||||
},
|
||||
load() {
|
||||
if (_src)
|
||||
engine.load(_src)
|
||||
},
|
||||
|
||||
destroy() {
|
||||
engine.destroy()
|
||||
},
|
||||
|
||||
get videoWidth() {
|
||||
return engine.videoWidth
|
||||
},
|
||||
get videoHeight() {
|
||||
return engine.videoHeight
|
||||
},
|
||||
|
||||
get playsInline() {
|
||||
return true
|
||||
},
|
||||
set playsInline(v) {},
|
||||
|
||||
get autoplay() {
|
||||
return option.autoplay || false
|
||||
},
|
||||
set autoplay(v) {},
|
||||
|
||||
get loop() {
|
||||
return option.loop || false
|
||||
},
|
||||
set loop(v) {},
|
||||
|
||||
get controls() {
|
||||
return false
|
||||
},
|
||||
set controls(v) {},
|
||||
|
||||
get crossOrigin() {
|
||||
return option.crossOrigin || ''
|
||||
},
|
||||
set crossOrigin(v) {},
|
||||
|
||||
get currentSrc() {
|
||||
return _src
|
||||
},
|
||||
|
||||
get poster() {
|
||||
return option.poster || ''
|
||||
},
|
||||
set poster(v) {
|
||||
option.poster = v
|
||||
},
|
||||
|
||||
get preload() {
|
||||
return 'auto'
|
||||
},
|
||||
set preload(v) {},
|
||||
|
||||
get defaultMuted() {
|
||||
return false
|
||||
},
|
||||
set defaultMuted(v) {},
|
||||
|
||||
get defaultPlaybackRate() {
|
||||
return 1
|
||||
},
|
||||
set defaultPlaybackRate(v) {},
|
||||
|
||||
canPlayType(_type) {
|
||||
return 'maybe'
|
||||
},
|
||||
|
||||
getBoundingClientRect() {
|
||||
return canvas.getBoundingClientRect()
|
||||
},
|
||||
|
||||
requestVideoFrameCallback(callback) {
|
||||
const id = requestAnimationFrame((time) => {
|
||||
callback(time, {
|
||||
presentationTime: engine.currentTime,
|
||||
expectedDisplayTime: time + 16.6,
|
||||
width: engine.videoWidth,
|
||||
height: engine.videoHeight,
|
||||
mediaTime: engine.currentTime,
|
||||
presentedFrames: 0,
|
||||
processingDuration: 0,
|
||||
captureTime: time,
|
||||
receiveTime: time,
|
||||
rtpTimestamp: 0,
|
||||
})
|
||||
})
|
||||
return id
|
||||
},
|
||||
|
||||
cancelVideoFrameCallback(id) {
|
||||
cancelAnimationFrame(id)
|
||||
},
|
||||
|
||||
setAttribute(name, value) {
|
||||
if (name === 'src')
|
||||
this.src = value
|
||||
else if (name === 'autoplay')
|
||||
this.autoplay = value
|
||||
else if (name === 'loop')
|
||||
this.loop = value
|
||||
else if (name === 'muted')
|
||||
this.muted = true
|
||||
else
|
||||
canvas.setAttribute(name, value)
|
||||
},
|
||||
}
|
||||
|
||||
if (option.source) {
|
||||
shim.src = option.source
|
||||
}
|
||||
else if (art.option?.url) {
|
||||
shim.src = art.option.url
|
||||
}
|
||||
|
||||
return shim
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
export function createVideoEventTarget() {
|
||||
const listeners = new Map()
|
||||
|
||||
function addEventListener(type, fn) {
|
||||
if (!listeners.has(type))
|
||||
listeners.set(type, [])
|
||||
listeners.get(type).push(fn)
|
||||
}
|
||||
|
||||
function removeEventListener(type, fn) {
|
||||
const list = listeners.get(type)
|
||||
if (!list)
|
||||
return
|
||||
const i = list.indexOf(fn)
|
||||
if (i >= 0)
|
||||
list.splice(i, 1)
|
||||
}
|
||||
|
||||
function emit(type, detail) {
|
||||
const evt = new Event(type)
|
||||
evt.detail = detail
|
||||
const list = listeners.get(type)
|
||||
if (list)
|
||||
list.forEach(fn => fn(evt))
|
||||
}
|
||||
|
||||
return { addEventListener, removeEventListener, emit }
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function clamp(v, min, max) {
|
||||
return Math.max(min, Math.min(max, Number(v) || 0))
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export default function sleep(ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms)
|
||||
})
|
||||
}
|
||||
@@ -1,14 +1,74 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
|
||||
interface Option {
|
||||
eventDelay?: number
|
||||
/**
|
||||
* Timeout for loading media in milliseconds
|
||||
* @default 12000
|
||||
*/
|
||||
loadTimeout?: number
|
||||
|
||||
/**
|
||||
* Interval for timeupdate events in milliseconds
|
||||
* @default 250
|
||||
*/
|
||||
timeupdateInterval?: number
|
||||
|
||||
/**
|
||||
* Audio-video synchronization tolerance in seconds
|
||||
* @default 0.12
|
||||
*/
|
||||
avSyncTolerance?: number
|
||||
|
||||
/**
|
||||
* Whether to drop late video frames
|
||||
* @default false
|
||||
*/
|
||||
dropLateFrames?: boolean
|
||||
|
||||
/**
|
||||
* Poster image URL
|
||||
*/
|
||||
poster?: string
|
||||
|
||||
/**
|
||||
* Media source (URL, Blob, or ReadableStream)
|
||||
*/
|
||||
source?: string | Blob | ReadableStream<Uint8Array>
|
||||
|
||||
/**
|
||||
* Check if server supports range requests before loading
|
||||
* @default false
|
||||
*/
|
||||
preflightRange?: boolean
|
||||
|
||||
/**
|
||||
* Initial volume (0-1)
|
||||
* @default 0.7
|
||||
*/
|
||||
volume?: number
|
||||
|
||||
/**
|
||||
* Initial muted state
|
||||
* @default false
|
||||
*/
|
||||
muted?: boolean
|
||||
|
||||
/**
|
||||
* Autoplay
|
||||
* @default false
|
||||
*/
|
||||
autoplay?: boolean
|
||||
|
||||
/**
|
||||
* Loop playback
|
||||
* @default false
|
||||
*/
|
||||
loop?: boolean
|
||||
|
||||
/**
|
||||
* Cross-origin setting
|
||||
*/
|
||||
crossOrigin?: string
|
||||
}
|
||||
|
||||
type Result = HTMLCanvasElement
|
||||
|
||||
Reference in new issue
Block a user