mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-09 03:46:16 -08:00
refactor(audio): [PKG-AUDIO-03] separate track ownership and native lifecycle
This commit is contained in:
1 parent
803e87252e
commit
9c07ea830c
20 files changed
+12654
-250
No files matched your search
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,o){"object"===typeof exports&&"undefined"!==typeof module?module.exports=o():"function"==typeof define&&define.amd?(t.artplayerPluginAudioTrack=o(),define(function(){return t.artplayerPluginAudioTrack})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAudioTrack=o()}(this,function(){"use strict";return function(e){return o=>{let{url:n,offset:a=0,sync:t=.3}=e;const u=new Audio;function i(){if(!o.video||!n)return;const e=o.currentTime+a;Math.abs(u.currentTime-e)>t&&(u.currentTime=e)}return u.preload="auto",n&&(u.src=n),o.on("play",()=>{n&&(i(),u.play().catch(e=>{console.warn(e)}))}),o.on("pause",()=>{u.pause()}),o.on("seek",()=>{i()}),o.on("video:timeupdate",()=>{o.playing&&i()}),o.on("video:ratechange",()=>{u.playbackRate=o.video.playbackRate}),o.on("video:volumechange",()=>{u.volume=o.volume,u.muted=o.muted}),o.on("video:waiting",()=>{u.pause()}),o.on("video:playing",()=>{n&&o.playing&&u.play().catch(e=>{console.warn(e)})}),o.on("destroy",()=>{u.pause(),u.src="",u.load()}),u.volume=o.volume,u.muted=o.muted,u.playbackRate=o.video?.playbackRate||1,{name:"artplayerPluginAudioTrack",audio:u,update:function(e){e.url&&e.url!==n&&(n=e.url,u.src=n,o.playing&&u.play().catch(e=>console.warn(e))),void 0!==e.offset&&(a=e.offset),void 0!==e.sync&&(t=e.sync)}}}}});
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAudioTrack=t(),define(function(){return e.artplayerPluginAudioTrack})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAudioTrack=t()}(this,function(){"use strict";return function(e){return t=>{const o=function(e){let{url:t,offset:o=0,sync:n=.3}=e;const a=new Audio;let i=!1;function u(){!i&&t&&a.play().catch(e=>{i||console.warn(e)})}function c(){if(i)return;i=!0;const e=[];for(const o of[()=>a.pause(),()=>a.removeAttribute("src"),()=>a.load()])try{o()}catch(t){e.push(t)}if(e.length)throw e[0]}try{a.preload="auto",t&&(a.src=t)}catch(r){try{c()}catch{}throw r}return{audio:a,play:u,pause:function(){i||a.pause()},destroy:c,sync(e){if(i||!t)return;const u=e+o;Math.abs(a.currentTime-u)>n&&(a.currentTime=u)},update(e,c){i||(e.url&&e.url!==t&&(t=e.url,a.src=t,c&&u()),void 0!==e.offset&&(o=e.offset),void 0!==e.sync&&(n=e.sync))}}}(e),{audio:n}=o,a=[];let i=!0;function u(){t.video&&o.sync(t.currentTime)}function c(e,o){const n=()=>{i&&o()};a.push([e,n]),t.on(e,n)}function r(){if(!i)return;i=!1;const e=[];for(const[o,i]of a.splice(0))try{t.off(o,i)}catch(n){e.push(n)}try{o.destroy()}catch(n){e.push(n)}if(e.length)throw e[0]}try{c("play",()=>{u(),o.play()});for(const e of["pause","video:pause","video:ended","video:waiting","video:emptied","video:seeking"])c(e,o.pause);c("seek",u),c("video:seeked",()=>{u(),t.playing&&o.play()}),c("video:timeupdate",()=>{t.playing&&u()}),c("video:ratechange",()=>{n.playbackRate=t.video.playbackRate}),c("video:volumechange",()=>{n.volume=t.volume,n.muted=t.muted}),c("video:playing",()=>{t.playing&&(u(),o.play())}),c("destroy",r),n.volume=t.volume,n.muted=t.muted,n.playbackRate=t.video?.playbackRate||1}catch(d){try{r()}catch{}throw d}return{name:"artplayerPluginAudioTrack",audio:n,update(e){i&&o.update(e,t.playing)}}}}});
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,o){"object"===typeof exports&&"undefined"!==typeof module?module.exports=o():"function"==typeof define&&define.amd?(t.artplayerPluginAudioTrack=o(),define(function(){return t.artplayerPluginAudioTrack})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAudioTrack=o()}(this,function(){"use strict";return function(e){return o=>{var n;let{url:a,offset:t=0,sync:u=.3}=e;const i=new Audio;function l(){if(!o.video||!a)return;const e=o.currentTime+t;Math.abs(i.currentTime-e)>u&&(i.currentTime=e)}return i.preload="auto",a&&(i.src=a),o.on("play",()=>{a&&(l(),i.play().catch(e=>{console.warn(e)}))}),o.on("pause",()=>{i.pause()}),o.on("seek",()=>{l()}),o.on("video:timeupdate",()=>{o.playing&&l()}),o.on("video:ratechange",()=>{i.playbackRate=o.video.playbackRate}),o.on("video:volumechange",()=>{i.volume=o.volume,i.muted=o.muted}),o.on("video:waiting",()=>{i.pause()}),o.on("video:playing",()=>{a&&o.playing&&i.play().catch(e=>{console.warn(e)})}),o.on("destroy",()=>{i.pause(),i.src="",i.load()}),i.volume=o.volume,i.muted=o.muted,i.playbackRate=(null==(n=o.video)?void 0:n.playbackRate)||1,{name:"artplayerPluginAudioTrack",audio:i,update:function(e){e.url&&e.url!==a&&(a=e.url,i.src=a,o.playing&&i.play().catch(e=>console.warn(e))),void 0!==e.offset&&(t=e.offset),void 0!==e.sync&&(u=e.sync)}}}}});
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAudioTrack=t(),define(function(){return e.artplayerPluginAudioTrack})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAudioTrack=t()}(this,function(){"use strict";return function(e){return t=>{var o;const n=function(e){let{url:t,offset:o=0,sync:n=.3}=e;const a=new Audio;let i=!1;function u(){!i&&t&&a.play().catch(e=>{i||console.warn(e)})}function c(){if(i)return;i=!0;const e=[];for(const o of[()=>a.pause(),()=>a.removeAttribute("src"),()=>a.load()])try{o()}catch(t){e.push(t)}if(e.length)throw e[0]}try{a.preload="auto",t&&(a.src=t)}catch(r){try{c()}catch(d){}throw r}return{audio:a,play:u,pause:function(){i||a.pause()},destroy:c,sync(e){if(i||!t)return;const u=e+o;Math.abs(a.currentTime-u)>n&&(a.currentTime=u)},update(e,c){i||(e.url&&e.url!==t&&(t=e.url,a.src=t,c&&u()),void 0!==e.offset&&(o=e.offset),void 0!==e.sync&&(n=e.sync))}}}(e),{audio:a}=n,i=[];let u=!0;function c(){t.video&&n.sync(t.currentTime)}function r(e,o){const n=()=>{u&&o()};i.push([e,n]),t.on(e,n)}function d(){if(!u)return;u=!1;const e=[];for(const[n,a]of i.splice(0))try{t.off(n,a)}catch(o){e.push(o)}try{n.destroy()}catch(o){e.push(o)}if(e.length)throw e[0]}try{r("play",()=>{c(),n.play()});for(const e of["pause","video:pause","video:ended","video:waiting","video:emptied","video:seeking"])r(e,n.pause);r("seek",c),r("video:seeked",()=>{c(),t.playing&&n.play()}),r("video:timeupdate",()=>{t.playing&&c()}),r("video:ratechange",()=>{a.playbackRate=t.video.playbackRate}),r("video:volumechange",()=>{a.volume=t.volume,a.muted=t.muted}),r("video:playing",()=>{t.playing&&(c(),n.play())}),r("destroy",d),a.volume=t.volume,a.muted=t.muted,a.playbackRate=(null==(o=t.video)?void 0:o.playbackRate)||1}catch(s){try{d()}catch(l){}throw s}return{name:"artplayerPluginAudioTrack",audio:a,update(e){u&&n.update(e,t.playing)}}}}});
|
||||
@@ -4,86 +4,162 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
function artplayerPluginAudioTrack(option) {
|
||||
return (art) => {
|
||||
let { url, offset = 0, sync = 0.3 } = option;
|
||||
const audio = new Audio();
|
||||
function createAudioTrack(option) {
|
||||
let { url, offset = 0, sync = 0.3 } = option;
|
||||
const audio = new Audio();
|
||||
let closed = false;
|
||||
function play() {
|
||||
if (closed || !url)
|
||||
return;
|
||||
audio.play().catch((error) => {
|
||||
if (!closed)
|
||||
console.warn(error);
|
||||
});
|
||||
}
|
||||
function pause() {
|
||||
if (!closed)
|
||||
audio.pause();
|
||||
}
|
||||
function destroy() {
|
||||
if (closed)
|
||||
return;
|
||||
closed = true;
|
||||
const failures = [];
|
||||
for (const release of [() => audio.pause(), () => audio.removeAttribute("src"), () => audio.load()]) {
|
||||
try {
|
||||
release();
|
||||
} catch (error) {
|
||||
failures.push(error);
|
||||
}
|
||||
}
|
||||
if (failures.length)
|
||||
throw failures[0];
|
||||
}
|
||||
try {
|
||||
audio.preload = "auto";
|
||||
if (url) {
|
||||
if (url)
|
||||
audio.src = url;
|
||||
} catch (error) {
|
||||
try {
|
||||
destroy();
|
||||
} catch {
|
||||
}
|
||||
function syncAudio() {
|
||||
if (!art.video || !url)
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
audio,
|
||||
play,
|
||||
pause,
|
||||
destroy,
|
||||
sync(time) {
|
||||
if (closed || !url)
|
||||
return;
|
||||
const videoTime = art.currentTime;
|
||||
const targetTime = videoTime + offset;
|
||||
if (Math.abs(audio.currentTime - targetTime) > sync) {
|
||||
audio.currentTime = targetTime;
|
||||
}
|
||||
}
|
||||
art.on("play", () => {
|
||||
if (!url)
|
||||
const target = time + offset;
|
||||
if (Math.abs(audio.currentTime - target) > sync)
|
||||
audio.currentTime = target;
|
||||
},
|
||||
update(newOption, playing) {
|
||||
if (closed)
|
||||
return;
|
||||
syncAudio();
|
||||
audio.play().catch((err) => {
|
||||
console.warn(err);
|
||||
});
|
||||
});
|
||||
art.on("pause", () => {
|
||||
audio.pause();
|
||||
});
|
||||
art.on("seek", () => {
|
||||
syncAudio();
|
||||
});
|
||||
art.on("video:timeupdate", () => {
|
||||
if (art.playing) {
|
||||
syncAudio();
|
||||
}
|
||||
});
|
||||
art.on("video:ratechange", () => {
|
||||
audio.playbackRate = art.video.playbackRate;
|
||||
});
|
||||
art.on("video:volumechange", () => {
|
||||
audio.volume = art.volume;
|
||||
audio.muted = art.muted;
|
||||
});
|
||||
art.on("video:waiting", () => {
|
||||
audio.pause();
|
||||
});
|
||||
art.on("video:playing", () => {
|
||||
if (url && art.playing) {
|
||||
audio.play().catch((err) => {
|
||||
console.warn(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
art.on("destroy", () => {
|
||||
audio.pause();
|
||||
audio.src = "";
|
||||
audio.load();
|
||||
});
|
||||
function update(newOption) {
|
||||
if (newOption.url && newOption.url !== url) {
|
||||
url = newOption.url;
|
||||
audio.src = url;
|
||||
if (art.playing) {
|
||||
audio.play().catch((err) => console.warn(err));
|
||||
if (playing)
|
||||
play();
|
||||
}
|
||||
if (newOption.offset !== void 0)
|
||||
offset = newOption.offset;
|
||||
if (newOption.sync !== void 0)
|
||||
sync = newOption.sync;
|
||||
}
|
||||
};
|
||||
}
|
||||
function artplayerPluginAudioTrack(option) {
|
||||
return (art) => {
|
||||
const track = createAudioTrack(option);
|
||||
const { audio } = track;
|
||||
const subscriptions = [];
|
||||
let active = true;
|
||||
function syncAudio() {
|
||||
if (art.video)
|
||||
track.sync(art.currentTime);
|
||||
}
|
||||
function listen(event, callback) {
|
||||
const listener = () => {
|
||||
if (active)
|
||||
callback();
|
||||
};
|
||||
subscriptions.push([event, listener]);
|
||||
art.on(event, listener);
|
||||
}
|
||||
function destroy() {
|
||||
if (!active)
|
||||
return;
|
||||
active = false;
|
||||
const failures = [];
|
||||
for (const [event, listener] of subscriptions.splice(0)) {
|
||||
try {
|
||||
art.off(event, listener);
|
||||
} catch (error) {
|
||||
failures.push(error);
|
||||
}
|
||||
}
|
||||
if (newOption.offset !== void 0) {
|
||||
offset = newOption.offset;
|
||||
}
|
||||
if (newOption.sync !== void 0) {
|
||||
sync = newOption.sync;
|
||||
try {
|
||||
track.destroy();
|
||||
} catch (error) {
|
||||
failures.push(error);
|
||||
}
|
||||
if (failures.length)
|
||||
throw failures[0];
|
||||
}
|
||||
try {
|
||||
listen("play", () => {
|
||||
syncAudio();
|
||||
track.play();
|
||||
});
|
||||
for (const event of ["pause", "video:pause", "video:ended", "video:waiting", "video:emptied", "video:seeking"])
|
||||
listen(event, track.pause);
|
||||
listen("seek", syncAudio);
|
||||
listen("video:seeked", () => {
|
||||
syncAudio();
|
||||
if (art.playing)
|
||||
track.play();
|
||||
});
|
||||
listen("video:timeupdate", () => {
|
||||
if (art.playing)
|
||||
syncAudio();
|
||||
});
|
||||
listen("video:ratechange", () => {
|
||||
audio.playbackRate = art.video.playbackRate;
|
||||
});
|
||||
listen("video:volumechange", () => {
|
||||
audio.volume = art.volume;
|
||||
audio.muted = art.muted;
|
||||
});
|
||||
listen("video:playing", () => {
|
||||
if (art.playing) {
|
||||
syncAudio();
|
||||
track.play();
|
||||
}
|
||||
});
|
||||
listen("destroy", destroy);
|
||||
audio.volume = art.volume;
|
||||
audio.muted = art.muted;
|
||||
audio.playbackRate = art.video?.playbackRate || 1;
|
||||
} catch (error) {
|
||||
try {
|
||||
destroy();
|
||||
} catch {
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
audio.volume = art.volume;
|
||||
audio.muted = art.muted;
|
||||
audio.playbackRate = art.video?.playbackRate || 1;
|
||||
return {
|
||||
name: "artplayerPluginAudioTrack",
|
||||
audio,
|
||||
update
|
||||
update(newOption) {
|
||||
if (active)
|
||||
track.update(newOption, art.playing);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Audio Track maintenance
|
||||
|
||||
The default factory in src/index.js keeps the public `(option) => (art) => result`
|
||||
contract. The result exposes the same HTMLAudioElement for its entire lifetime, a
|
||||
literal name, and synchronous update. Public declarations remain authored in types/
|
||||
until the separate type migration; do not infer new public methods from internal helpers.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- src/index.js owns ArtPlayer subscriptions, reads host media/volume/rate, adapts public
|
||||
update, and rolls back partial installation. It uses only on/off and existing media
|
||||
properties, so it does not require new core lifecycle utilities.
|
||||
- src/track.js creates the Audio element and owns URL/offset/threshold and closed state.
|
||||
It handles synchronization, play rejection and source updates. It does not import ArtPlayer,
|
||||
inspect DOM registries or install listeners. Dependencies flow from the entry to the track.
|
||||
|
||||
There are no custom timers, UI nodes, styles, workers or playback SDKs. Browser media
|
||||
loading belongs to the exposed Audio element. Changing its URL preserves element identity.
|
||||
The plugin does not mute the main video; consumers still choose their own mixing behavior.
|
||||
|
||||
## Events and lifetime
|
||||
|
||||
Public play synchronizes before audio playback. Native pause/ended/waiting/emptied/seeking
|
||||
pause audio; seeked synchronizes and resumes only if the video is playing. Native playing
|
||||
synchronizes again before resume. Ordinary playing timeupdate retains the strict drift
|
||||
threshold and offset rule. Rate and volume/mute follow the existing core properties.
|
||||
|
||||
Destroy marks the entry inactive before removing listeners. The track then closes before
|
||||
pausing, removing the src attribute and loading the empty element. Cleanup attempts all
|
||||
steps even when a consumer-overridden method throws, then propagates the first cleanup error.
|
||||
Installation failures preserve the original error after attempting rollback. Saved callbacks
|
||||
and update are inert after close, including when a host off method fails. Rejections from
|
||||
an already closed track are consumed; active play failures still warn with the original error.
|
||||
No promise is added to the public update return value.
|
||||
|
||||
URL truthiness/same-URL handling, defaults, deferred offset/sync update and exposed audio are
|
||||
preserved. Negative offset start and duration boundaries are not redesigned in this lifecycle
|
||||
change; their remaining media validation is tracked in refactor/audio-validation.md.
|
||||
|
||||
## Change and verify
|
||||
|
||||
Edit track.js for URL/sync/media ownership and index.js for host event handling. Keep
|
||||
published-only defect observations in the tests when adding candidate regressions.
|
||||
|
||||
```sh
|
||||
node --test test/audio-track.test.js
|
||||
yarn build artplayer-plugin-audio-track
|
||||
yarn test:browser test/browser/audio-track.spec.js --trace on
|
||||
yarn ci:check
|
||||
```
|
||||
|
||||
ARTPLAYER_TEST_AUDIO accepts platform-delimited built filenames for all three Node artifact
|
||||
formats. ARTPLAYER_AUDIO_ARTIFACT selects one main/legacy browser build; otherwise tests compile
|
||||
source. These are explicit local artifacts, not isolated installed npm packages.
|
||||
The browser suite includes frozen old core/plugin combinations, real AAC/decoded video,
|
||||
source failure recovery, and old/native pause/end plus corrected candidate cleanup.
|
||||
See refactor/audio-validation.md for failed baseline probes, Windows WebKit WAV/size limits,
|
||||
physical-device gaps and the separate 04/05/06 type/combination/distribution gates.
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,o){"object"===typeof exports&&"undefined"!==typeof module?module.exports=o():"function"==typeof define&&define.amd?(t.artplayerPluginAudioTrack=o(),define(function(){return t.artplayerPluginAudioTrack})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAudioTrack=o()}(this,function(){"use strict";return function(e){return o=>{let{url:n,offset:a=0,sync:t=.3}=e;const u=new Audio;function i(){if(!o.video||!n)return;const e=o.currentTime+a;Math.abs(u.currentTime-e)>t&&(u.currentTime=e)}return u.preload="auto",n&&(u.src=n),o.on("play",()=>{n&&(i(),u.play().catch(e=>{console.warn(e)}))}),o.on("pause",()=>{u.pause()}),o.on("seek",()=>{i()}),o.on("video:timeupdate",()=>{o.playing&&i()}),o.on("video:ratechange",()=>{u.playbackRate=o.video.playbackRate}),o.on("video:volumechange",()=>{u.volume=o.volume,u.muted=o.muted}),o.on("video:waiting",()=>{u.pause()}),o.on("video:playing",()=>{n&&o.playing&&u.play().catch(e=>{console.warn(e)})}),o.on("destroy",()=>{u.pause(),u.src="",u.load()}),u.volume=o.volume,u.muted=o.muted,u.playbackRate=o.video?.playbackRate||1,{name:"artplayerPluginAudioTrack",audio:u,update:function(e){e.url&&e.url!==n&&(n=e.url,u.src=n,o.playing&&u.play().catch(e=>console.warn(e))),void 0!==e.offset&&(a=e.offset),void 0!==e.sync&&(t=e.sync)}}}}});
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAudioTrack=t(),define(function(){return e.artplayerPluginAudioTrack})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAudioTrack=t()}(this,function(){"use strict";return function(e){return t=>{const o=function(e){let{url:t,offset:o=0,sync:n=.3}=e;const a=new Audio;let i=!1;function u(){!i&&t&&a.play().catch(e=>{i||console.warn(e)})}function c(){if(i)return;i=!0;const e=[];for(const o of[()=>a.pause(),()=>a.removeAttribute("src"),()=>a.load()])try{o()}catch(t){e.push(t)}if(e.length)throw e[0]}try{a.preload="auto",t&&(a.src=t)}catch(r){try{c()}catch{}throw r}return{audio:a,play:u,pause:function(){i||a.pause()},destroy:c,sync(e){if(i||!t)return;const u=e+o;Math.abs(a.currentTime-u)>n&&(a.currentTime=u)},update(e,c){i||(e.url&&e.url!==t&&(t=e.url,a.src=t,c&&u()),void 0!==e.offset&&(o=e.offset),void 0!==e.sync&&(n=e.sync))}}}(e),{audio:n}=o,a=[];let i=!0;function u(){t.video&&o.sync(t.currentTime)}function c(e,o){const n=()=>{i&&o()};a.push([e,n]),t.on(e,n)}function r(){if(!i)return;i=!1;const e=[];for(const[o,i]of a.splice(0))try{t.off(o,i)}catch(n){e.push(n)}try{o.destroy()}catch(n){e.push(n)}if(e.length)throw e[0]}try{c("play",()=>{u(),o.play()});for(const e of["pause","video:pause","video:ended","video:waiting","video:emptied","video:seeking"])c(e,o.pause);c("seek",u),c("video:seeked",()=>{u(),t.playing&&o.play()}),c("video:timeupdate",()=>{t.playing&&u()}),c("video:ratechange",()=>{n.playbackRate=t.video.playbackRate}),c("video:volumechange",()=>{n.volume=t.volume,n.muted=t.muted}),c("video:playing",()=>{t.playing&&(u(),o.play())}),c("destroy",r),n.volume=t.volume,n.muted=t.muted,n.playbackRate=t.video?.playbackRate||1}catch(d){try{r()}catch{}throw d}return{name:"artplayerPluginAudioTrack",audio:n,update(e){i&&o.update(e,t.playing)}}}}});
|
||||
+1
-1
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,o){"object"===typeof exports&&"undefined"!==typeof module?module.exports=o():"function"==typeof define&&define.amd?(t.artplayerPluginAudioTrack=o(),define(function(){return t.artplayerPluginAudioTrack})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAudioTrack=o()}(this,function(){"use strict";return function(e){return o=>{var n;let{url:a,offset:t=0,sync:u=.3}=e;const i=new Audio;function l(){if(!o.video||!a)return;const e=o.currentTime+t;Math.abs(i.currentTime-e)>u&&(i.currentTime=e)}return i.preload="auto",a&&(i.src=a),o.on("play",()=>{a&&(l(),i.play().catch(e=>{console.warn(e)}))}),o.on("pause",()=>{i.pause()}),o.on("seek",()=>{l()}),o.on("video:timeupdate",()=>{o.playing&&l()}),o.on("video:ratechange",()=>{i.playbackRate=o.video.playbackRate}),o.on("video:volumechange",()=>{i.volume=o.volume,i.muted=o.muted}),o.on("video:waiting",()=>{i.pause()}),o.on("video:playing",()=>{a&&o.playing&&i.play().catch(e=>{console.warn(e)})}),o.on("destroy",()=>{i.pause(),i.src="",i.load()}),i.volume=o.volume,i.muted=o.muted,i.playbackRate=(null==(n=o.video)?void 0:n.playbackRate)||1,{name:"artplayerPluginAudioTrack",audio:i,update:function(e){e.url&&e.url!==a&&(a=e.url,i.src=a,o.playing&&i.play().catch(e=>console.warn(e))),void 0!==e.offset&&(t=e.offset),void 0!==e.sync&&(u=e.sync)}}}}});
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginAudioTrack=t(),define(function(){return e.artplayerPluginAudioTrack})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAudioTrack=t()}(this,function(){"use strict";return function(e){return t=>{var o;const n=function(e){let{url:t,offset:o=0,sync:n=.3}=e;const a=new Audio;let i=!1;function u(){!i&&t&&a.play().catch(e=>{i||console.warn(e)})}function c(){if(i)return;i=!0;const e=[];for(const o of[()=>a.pause(),()=>a.removeAttribute("src"),()=>a.load()])try{o()}catch(t){e.push(t)}if(e.length)throw e[0]}try{a.preload="auto",t&&(a.src=t)}catch(r){try{c()}catch(d){}throw r}return{audio:a,play:u,pause:function(){i||a.pause()},destroy:c,sync(e){if(i||!t)return;const u=e+o;Math.abs(a.currentTime-u)>n&&(a.currentTime=u)},update(e,c){i||(e.url&&e.url!==t&&(t=e.url,a.src=t,c&&u()),void 0!==e.offset&&(o=e.offset),void 0!==e.sync&&(n=e.sync))}}}(e),{audio:a}=n,i=[];let u=!0;function c(){t.video&&n.sync(t.currentTime)}function r(e,o){const n=()=>{u&&o()};i.push([e,n]),t.on(e,n)}function d(){if(!u)return;u=!1;const e=[];for(const[n,a]of i.splice(0))try{t.off(n,a)}catch(o){e.push(o)}try{n.destroy()}catch(o){e.push(o)}if(e.length)throw e[0]}try{r("play",()=>{c(),n.play()});for(const e of["pause","video:pause","video:ended","video:waiting","video:emptied","video:seeking"])r(e,n.pause);r("seek",c),r("video:seeked",()=>{c(),t.playing&&n.play()}),r("video:timeupdate",()=>{t.playing&&c()}),r("video:ratechange",()=>{a.playbackRate=t.video.playbackRate}),r("video:volumechange",()=>{a.volume=t.volume,a.muted=t.muted}),r("video:playing",()=>{t.playing&&(c(),n.play())}),r("destroy",d),a.volume=t.volume,a.muted=t.muted,a.playbackRate=(null==(o=t.video)?void 0:o.playbackRate)||1}catch(s){try{d()}catch(l){}throw s}return{name:"artplayerPluginAudioTrack",audio:a,update(e){u&&n.update(e,t.playing)}}}}});
|
||||
+141
-65
@@ -4,86 +4,162 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
function artplayerPluginAudioTrack(option) {
|
||||
return (art) => {
|
||||
let { url, offset = 0, sync = 0.3 } = option;
|
||||
const audio = new Audio();
|
||||
function createAudioTrack(option) {
|
||||
let { url, offset = 0, sync = 0.3 } = option;
|
||||
const audio = new Audio();
|
||||
let closed = false;
|
||||
function play() {
|
||||
if (closed || !url)
|
||||
return;
|
||||
audio.play().catch((error) => {
|
||||
if (!closed)
|
||||
console.warn(error);
|
||||
});
|
||||
}
|
||||
function pause() {
|
||||
if (!closed)
|
||||
audio.pause();
|
||||
}
|
||||
function destroy() {
|
||||
if (closed)
|
||||
return;
|
||||
closed = true;
|
||||
const failures = [];
|
||||
for (const release of [() => audio.pause(), () => audio.removeAttribute("src"), () => audio.load()]) {
|
||||
try {
|
||||
release();
|
||||
} catch (error) {
|
||||
failures.push(error);
|
||||
}
|
||||
}
|
||||
if (failures.length)
|
||||
throw failures[0];
|
||||
}
|
||||
try {
|
||||
audio.preload = "auto";
|
||||
if (url) {
|
||||
if (url)
|
||||
audio.src = url;
|
||||
} catch (error) {
|
||||
try {
|
||||
destroy();
|
||||
} catch {
|
||||
}
|
||||
function syncAudio() {
|
||||
if (!art.video || !url)
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
audio,
|
||||
play,
|
||||
pause,
|
||||
destroy,
|
||||
sync(time) {
|
||||
if (closed || !url)
|
||||
return;
|
||||
const videoTime = art.currentTime;
|
||||
const targetTime = videoTime + offset;
|
||||
if (Math.abs(audio.currentTime - targetTime) > sync) {
|
||||
audio.currentTime = targetTime;
|
||||
}
|
||||
}
|
||||
art.on("play", () => {
|
||||
if (!url)
|
||||
const target = time + offset;
|
||||
if (Math.abs(audio.currentTime - target) > sync)
|
||||
audio.currentTime = target;
|
||||
},
|
||||
update(newOption, playing) {
|
||||
if (closed)
|
||||
return;
|
||||
syncAudio();
|
||||
audio.play().catch((err) => {
|
||||
console.warn(err);
|
||||
});
|
||||
});
|
||||
art.on("pause", () => {
|
||||
audio.pause();
|
||||
});
|
||||
art.on("seek", () => {
|
||||
syncAudio();
|
||||
});
|
||||
art.on("video:timeupdate", () => {
|
||||
if (art.playing) {
|
||||
syncAudio();
|
||||
}
|
||||
});
|
||||
art.on("video:ratechange", () => {
|
||||
audio.playbackRate = art.video.playbackRate;
|
||||
});
|
||||
art.on("video:volumechange", () => {
|
||||
audio.volume = art.volume;
|
||||
audio.muted = art.muted;
|
||||
});
|
||||
art.on("video:waiting", () => {
|
||||
audio.pause();
|
||||
});
|
||||
art.on("video:playing", () => {
|
||||
if (url && art.playing) {
|
||||
audio.play().catch((err) => {
|
||||
console.warn(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
art.on("destroy", () => {
|
||||
audio.pause();
|
||||
audio.src = "";
|
||||
audio.load();
|
||||
});
|
||||
function update(newOption) {
|
||||
if (newOption.url && newOption.url !== url) {
|
||||
url = newOption.url;
|
||||
audio.src = url;
|
||||
if (art.playing) {
|
||||
audio.play().catch((err) => console.warn(err));
|
||||
if (playing)
|
||||
play();
|
||||
}
|
||||
if (newOption.offset !== void 0)
|
||||
offset = newOption.offset;
|
||||
if (newOption.sync !== void 0)
|
||||
sync = newOption.sync;
|
||||
}
|
||||
};
|
||||
}
|
||||
function artplayerPluginAudioTrack(option) {
|
||||
return (art) => {
|
||||
const track = createAudioTrack(option);
|
||||
const { audio } = track;
|
||||
const subscriptions = [];
|
||||
let active = true;
|
||||
function syncAudio() {
|
||||
if (art.video)
|
||||
track.sync(art.currentTime);
|
||||
}
|
||||
function listen(event, callback) {
|
||||
const listener = () => {
|
||||
if (active)
|
||||
callback();
|
||||
};
|
||||
subscriptions.push([event, listener]);
|
||||
art.on(event, listener);
|
||||
}
|
||||
function destroy() {
|
||||
if (!active)
|
||||
return;
|
||||
active = false;
|
||||
const failures = [];
|
||||
for (const [event, listener] of subscriptions.splice(0)) {
|
||||
try {
|
||||
art.off(event, listener);
|
||||
} catch (error) {
|
||||
failures.push(error);
|
||||
}
|
||||
}
|
||||
if (newOption.offset !== void 0) {
|
||||
offset = newOption.offset;
|
||||
}
|
||||
if (newOption.sync !== void 0) {
|
||||
sync = newOption.sync;
|
||||
try {
|
||||
track.destroy();
|
||||
} catch (error) {
|
||||
failures.push(error);
|
||||
}
|
||||
if (failures.length)
|
||||
throw failures[0];
|
||||
}
|
||||
try {
|
||||
listen("play", () => {
|
||||
syncAudio();
|
||||
track.play();
|
||||
});
|
||||
for (const event of ["pause", "video:pause", "video:ended", "video:waiting", "video:emptied", "video:seeking"])
|
||||
listen(event, track.pause);
|
||||
listen("seek", syncAudio);
|
||||
listen("video:seeked", () => {
|
||||
syncAudio();
|
||||
if (art.playing)
|
||||
track.play();
|
||||
});
|
||||
listen("video:timeupdate", () => {
|
||||
if (art.playing)
|
||||
syncAudio();
|
||||
});
|
||||
listen("video:ratechange", () => {
|
||||
audio.playbackRate = art.video.playbackRate;
|
||||
});
|
||||
listen("video:volumechange", () => {
|
||||
audio.volume = art.volume;
|
||||
audio.muted = art.muted;
|
||||
});
|
||||
listen("video:playing", () => {
|
||||
if (art.playing) {
|
||||
syncAudio();
|
||||
track.play();
|
||||
}
|
||||
});
|
||||
listen("destroy", destroy);
|
||||
audio.volume = art.volume;
|
||||
audio.muted = art.muted;
|
||||
audio.playbackRate = art.video?.playbackRate || 1;
|
||||
} catch (error) {
|
||||
try {
|
||||
destroy();
|
||||
} catch {
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
audio.volume = art.volume;
|
||||
audio.muted = art.muted;
|
||||
audio.playbackRate = art.video?.playbackRate || 1;
|
||||
return {
|
||||
name: "artplayerPluginAudioTrack",
|
||||
audio,
|
||||
update
|
||||
update(newOption) {
|
||||
if (active)
|
||||
track.update(newOption, art.playing);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,101 +1,95 @@
|
||||
import { createAudioTrack } from './track'
|
||||
|
||||
export default function artplayerPluginAudioTrack(option) {
|
||||
return (art) => {
|
||||
let { url, offset = 0, sync = 0.3 } = option
|
||||
|
||||
const audio = new Audio()
|
||||
audio.preload = 'auto'
|
||||
if (url) {
|
||||
audio.src = url
|
||||
}
|
||||
const track = createAudioTrack(option)
|
||||
const { audio } = track
|
||||
const subscriptions = []
|
||||
let active = true
|
||||
|
||||
function syncAudio() {
|
||||
if (!art.video || !url)
|
||||
return
|
||||
|
||||
const videoTime = art.currentTime
|
||||
const targetTime = videoTime + offset
|
||||
|
||||
if (Math.abs(audio.currentTime - targetTime) > sync) {
|
||||
audio.currentTime = targetTime
|
||||
}
|
||||
if (art.video)
|
||||
track.sync(art.currentTime)
|
||||
}
|
||||
|
||||
art.on('play', () => {
|
||||
if (!url)
|
||||
return
|
||||
syncAudio()
|
||||
audio.play().catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
})
|
||||
|
||||
art.on('pause', () => {
|
||||
audio.pause()
|
||||
})
|
||||
|
||||
art.on('seek', () => {
|
||||
syncAudio()
|
||||
})
|
||||
|
||||
art.on('video:timeupdate', () => {
|
||||
if (art.playing) {
|
||||
syncAudio()
|
||||
function listen(event, callback) {
|
||||
const listener = () => {
|
||||
if (active)
|
||||
callback()
|
||||
}
|
||||
})
|
||||
subscriptions.push([event, listener])
|
||||
art.on(event, listener)
|
||||
}
|
||||
|
||||
art.on('video:ratechange', () => {
|
||||
audio.playbackRate = art.video.playbackRate
|
||||
})
|
||||
function destroy() {
|
||||
if (!active)
|
||||
return
|
||||
active = false
|
||||
const failures = []
|
||||
for (const [event, listener] of subscriptions.splice(0)) {
|
||||
try {
|
||||
art.off(event, listener)
|
||||
}
|
||||
catch (error) { failures.push(error) }
|
||||
}
|
||||
try {
|
||||
track.destroy()
|
||||
}
|
||||
catch (error) { failures.push(error) }
|
||||
if (failures.length)
|
||||
throw failures[0]
|
||||
}
|
||||
|
||||
art.on('video:volumechange', () => {
|
||||
try {
|
||||
listen('play', () => {
|
||||
syncAudio()
|
||||
track.play()
|
||||
})
|
||||
for (const event of ['pause', 'video:pause', 'video:ended', 'video:waiting', 'video:emptied', 'video:seeking'])
|
||||
listen(event, track.pause)
|
||||
listen('seek', syncAudio)
|
||||
listen('video:seeked', () => {
|
||||
syncAudio()
|
||||
if (art.playing)
|
||||
track.play()
|
||||
})
|
||||
listen('video:timeupdate', () => {
|
||||
if (art.playing)
|
||||
syncAudio()
|
||||
})
|
||||
listen('video:ratechange', () => {
|
||||
audio.playbackRate = art.video.playbackRate
|
||||
})
|
||||
listen('video:volumechange', () => {
|
||||
audio.volume = art.volume
|
||||
audio.muted = art.muted
|
||||
})
|
||||
listen('video:playing', () => {
|
||||
if (art.playing) {
|
||||
syncAudio()
|
||||
track.play()
|
||||
}
|
||||
})
|
||||
listen('destroy', destroy)
|
||||
audio.volume = art.volume
|
||||
audio.muted = art.muted
|
||||
})
|
||||
|
||||
art.on('video:waiting', () => {
|
||||
audio.pause()
|
||||
})
|
||||
|
||||
art.on('video:playing', () => {
|
||||
if (url && art.playing) {
|
||||
audio.play().catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
art.on('destroy', () => {
|
||||
audio.pause()
|
||||
audio.src = ''
|
||||
audio.load()
|
||||
})
|
||||
|
||||
function update(newOption) {
|
||||
if (newOption.url && newOption.url !== url) {
|
||||
url = newOption.url
|
||||
audio.src = url
|
||||
if (art.playing) {
|
||||
audio.play().catch(err => console.warn(err))
|
||||
}
|
||||
}
|
||||
|
||||
if (newOption.offset !== undefined) {
|
||||
offset = newOption.offset
|
||||
}
|
||||
|
||||
if (newOption.sync !== undefined) {
|
||||
sync = newOption.sync
|
||||
}
|
||||
audio.playbackRate = art.video?.playbackRate || 1
|
||||
}
|
||||
catch (error) {
|
||||
try {
|
||||
destroy()
|
||||
}
|
||||
catch {}
|
||||
throw error
|
||||
}
|
||||
|
||||
audio.volume = art.volume
|
||||
audio.muted = art.muted
|
||||
audio.playbackRate = art.video?.playbackRate || 1
|
||||
|
||||
return {
|
||||
name: 'artplayerPluginAudioTrack',
|
||||
audio,
|
||||
update,
|
||||
update(newOption) {
|
||||
if (active)
|
||||
track.update(newOption, art.playing)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
export function createAudioTrack(option) {
|
||||
let { url, offset = 0, sync = 0.3 } = option
|
||||
const audio = new Audio()
|
||||
let closed = false
|
||||
|
||||
function play() {
|
||||
if (closed || !url)
|
||||
return
|
||||
audio.play().catch((error) => {
|
||||
if (!closed)
|
||||
console.warn(error)
|
||||
})
|
||||
}
|
||||
|
||||
function pause() {
|
||||
if (!closed)
|
||||
audio.pause()
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (closed)
|
||||
return
|
||||
closed = true
|
||||
const failures = []
|
||||
for (const release of [() => audio.pause(), () => audio.removeAttribute('src'), () => audio.load()]) {
|
||||
try {
|
||||
release()
|
||||
}
|
||||
catch (error) { failures.push(error) }
|
||||
}
|
||||
if (failures.length)
|
||||
throw failures[0]
|
||||
}
|
||||
|
||||
try {
|
||||
audio.preload = 'auto'
|
||||
if (url)
|
||||
audio.src = url
|
||||
}
|
||||
catch (error) {
|
||||
try {
|
||||
destroy()
|
||||
}
|
||||
catch {}
|
||||
throw error
|
||||
}
|
||||
|
||||
return {
|
||||
audio,
|
||||
play,
|
||||
pause,
|
||||
destroy,
|
||||
sync(time) {
|
||||
if (closed || !url)
|
||||
return
|
||||
const target = time + offset
|
||||
if (Math.abs(audio.currentTime - target) > sync)
|
||||
audio.currentTime = target
|
||||
},
|
||||
update(newOption, playing) {
|
||||
if (closed)
|
||||
return
|
||||
if (newOption.url && newOption.url !== url) {
|
||||
url = newOption.url
|
||||
audio.src = url
|
||||
if (playing)
|
||||
play()
|
||||
}
|
||||
if (newOption.offset !== undefined)
|
||||
offset = newOption.offset
|
||||
if (newOption.sync !== undefined)
|
||||
sync = newOption.sync
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,16 @@
|
||||
# Audio Track 行为与媒体测试
|
||||
|
||||
## PKG-AUDIO-03 候选修复
|
||||
|
||||
音频状态已从核心事件接入拆出,源码 44 项、加入三种产物共 89 项 Node 通过。
|
||||
正式 main/legacy 各三引擎 42 项通过(均含原有 9 缺陷观察/3 原生诊断);CI 583 项通过。
|
||||
原生暂停/结束/直接 seek、无错误媒体清理和销毁后 update 已有候选回归。
|
||||
source 标签可由 ARTPLAYER_AUDIO_ARTIFACT 显式选择实际 main/legacy 文件;未设置时仍编译源码。
|
||||
ARTPLAYER_TEST_AUDIO 接受平台分隔的三个 Node 产物路径,候选生命周期用例也逐格式执行。
|
||||
具体输入、初轮六个错误 currentSrc 断言及最终证据见
|
||||
[生命周期记录](baselines/audio-lifecycle-validation.json)。AUDIO-LIFE-01 按守卫/空 src 范围关闭,
|
||||
AUDIO-SYNC-01 的边界/完整缓冲及设备验证继续 open。下面保留 02 当时的基线结果与问题描述。
|
||||
|
||||
PKG-AUDIO-02;公开来源见 [1.1.0 契约](baselines/audio-track-contract.md),
|
||||
固定执行结果及失败保留见 [验证记录](baselines/audio-validation.json)。
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,53 @@
|
||||
# PKG-AUDIO-03:音频同步与资源归属
|
||||
|
||||
状态:本步生命周期与结构验收完成;起点 803e8725。02 已冻结真实旧版缺陷。
|
||||
将媒体状态/操作与 ArtPlayer 事件安装分开,保留公开工厂、同一 audio、同步 update。
|
||||
修复原生暂停/结束后继续播放、销毁空 src 错误、销毁后的外部 update 和注册中断清理。
|
||||
本步仍为 JS,04 单独完成严格 TS/兼容声明。负偏移起点等未完整验证项继续由 05 接续,
|
||||
不为本次资源修复顺便改变偏移/空 URL/同 URL 更新语义。
|
||||
|
||||
## 实现和兼容
|
||||
|
||||
index.js 只负责读取核心状态、安装/释放订阅、公开 update 和失败回滚。
|
||||
track.js 拥有 Audio、URL/offset/sync、关闭状态与媒体操作;不接收整个 art、不依赖新核心工具。
|
||||
两个模块职责清楚,不为拆分额外创建纯转发模块。包内 ARCHITECTURE.md 给出实际入口、
|
||||
资源归属、时序、错误规则、回归命令及未完成事项。
|
||||
|
||||
原生 video:pause/ended/waiting/emptied/seeking 暂停音频;seeked/playing 重新同步并按主视频
|
||||
实际播放状态恢复。原公开 play/seek/timeupdate 及音量/倍率仍保留。
|
||||
销毁先关闭入口、解绑所有自有订阅,再关闭媒体状态、pause/removeAttribute('src')/load。
|
||||
每步清理独立尝试,正常销毁传播首个清理错误;初始化失败保留原始错误。
|
||||
关闭后的 update/保留事件回调/待决播放拒绝不会重新写入资源或警告;活动状态拒绝仍警告原错误。
|
||||
|
||||
保持工厂必传对象、注册/返回同步、公开 name/audio/update、同一元素引用、无自动主视频静音、
|
||||
空/同 URL 不重载、update 部分配置的运行时支持、默认值和严格漂移阈值。
|
||||
没有更改公开 manifest/声明(与 1.1.0 冻结 LF 哈希相同)或新增运行依赖;JS/TS 类型不一致
|
||||
由 04 处理。构建产物只通过正式 yarn build 生成,docs/compiled 三份与包内产物逐字节相同。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- Source Node 专项 44 项;含候选三格式后 89 项,均通过。六组候选生命周期回归也在
|
||||
main/legacy/ESM 执行,不只检查源码;旧版缺陷观察仍绑定旧 npm 归档。
|
||||
- 完整 CI 583 项(540+11+32)、254 生产 TS 通过。本步两个音频 JS 文件未计为 TS 完成。
|
||||
- main/legacy 各三引擎 42 项通过,无失败、重试或跳过;各包括 9 项旧缺陷观察、3 项原生诊断。
|
||||
新增候选验证确认直接暂停/结束停止音频、直接 currentTime seek 同步、关闭后引用无效及无媒体错误。
|
||||
- 正式构建通过;main 2102 字节/gzip 973,旧基线 1471/gzip 676。增加 297 gzip 字节用于
|
||||
原生事件与资源守卫,不称体积优化。所有分发 SHA 与样本/运行输入见冻结记录。
|
||||
- 定向 lint 通过;完整 CI 后仅修改浏览器清理断言,额外定向 lint 和两产物矩阵均通过。
|
||||
计划/风险表和 diff 检查通过:216 项任务、126 项风险,无未解决事项豁免。
|
||||
|
||||
证据:[audio-lifecycle-validation.json](../baselines/audio-lifecycle-validation.json)。
|
||||
首轮 36 通过、6 失败全部保留:测试错误要求只读 currentSrc 清空,而三个引擎的 audio
|
||||
和核心 video 均已 src 属性缺失、paused=true、readyState/networkState=0、error=null,
|
||||
currentSrc 仍保留旧值。改检查这些可观察资源状态,没有修改媒体源码、扩大等待或忽略错误。
|
||||
这也明确限定清理保证:不会伪造浏览器只读属性。
|
||||
|
||||
## 后续与交付
|
||||
|
||||
AUDIO-LIFE-01 按已登记范围关闭;不声称所有设备没有泄漏。
|
||||
AUDIO-SYNC-01 仅原生暂停/结束/seek 问题已修复,负偏移起点/时长边界、真实缓冲和连续切源
|
||||
仍由 05 保持 open。WAV/WebKit、设备、安装包、类型、示例和版本门槛继续保留。
|
||||
|
||||
独立提交主题:`refactor(audio): [PKG-AUDIO-03] separate track ownership and native lifecycle`。
|
||||
可回退本任务提交恢复旧生产行为及生成产物;不要回退旧发布基线或删除历史缺陷证据。
|
||||
接续 PKG-AUDIO-04,把这两个职责模块迁移严格 TS,补兼容公开声明和编辑器;无 push/tag/publish/merge。
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 216 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 148 / doing 1 / blocked 0 / done 67 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 147 / doing 1 / blocked 0 / done 68 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| PKG-AUDIO-01 | artplayer-plugin-audio-track<br>核对包契约与历史用法 | BASE-05 | url/offset/sync、audio 实例暴露、update 与音量语义 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
|
||||
| PKG-AUDIO-02 | artplayer-plugin-audio-track<br>建立特有行为与错误测试 | PKG-AUDIO-01, ENG-03, ENG-05 | 偏移、倍率、seek、waiting/playing、加载错误和 autoplay 拒绝 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
|
||||
| PKG-AUDIO-03 | artplayer-plugin-audio-track<br>整理内部职责与资源 | PKG-AUDIO-02, CORE-10 | 外部 audio 的同步策略、监听与源更新生命周期 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
|
||||
| PKG-AUDIO-03 | artplayer-plugin-audio-track<br>整理内部职责与资源 | PKG-AUDIO-02, CORE-10 | 外部 audio 的同步策略、监听与源更新生命周期 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
|
||||
| PKG-AUDIO-04 | artplayer-plugin-audio-track<br>迁移自有源码和公开类型 | PKG-AUDIO-03, ENG-04, ENG-06, CORE-07 | Option/Result/audio、update 输入和事件类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
|
||||
| PKG-AUDIO-05 | artplayer-plugin-audio-track<br>验证新旧核心和组合 | PKG-AUDIO-04, CORE-22 | 主视频切源和缓冲恢复,多实例独立音频、旧核心 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
|
||||
| PKG-AUDIO-06 | artplayer-plugin-audio-track<br>验证分发并同步文档 | PKG-AUDIO-05, ENG-07 | audio.track.js 示例与运行返回值一致的声明 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
|
||||
@@ -468,6 +468,7 @@
|
||||
- PKG-CHAPTER-04: [记录](changes/2026-09-10-PKG-CHAPTER-04-public-types.md) [记录](baselines/chapter-types-validation.json)
|
||||
- PKG-AUDIO-01: [记录](changes/2026-09-12-PKG-AUDIO-01-contract.md) [记录](baselines/audio-track-contract.md) [记录](baselines/audio-track-release.json)
|
||||
- PKG-AUDIO-02: [记录](changes/2026-09-12-PKG-AUDIO-02-tests.md) [记录](audio-validation.md) [记录](baselines/audio-validation.json)
|
||||
- PKG-AUDIO-03: [记录](changes/2026-09-12-PKG-AUDIO-03-lifecycle.md) [记录](baselines/audio-lifecycle-validation.json)
|
||||
- PKG-HLS-01: [记录](baselines/hls-control-contract.md) [记录](baselines/hls-control-release.json) [记录](changes/2026-09-11-PKG-HLS-01-contract.md)
|
||||
- PKG-HLS-02: [记录](hls-validation.md) [记录](baselines/hls-sdk.json) [记录](baselines/hls-validation.json) [记录](changes/2026-09-11-PKG-HLS-02-tests.md)
|
||||
- PKG-HLS-03: [记录](changes/2026-09-12-PKG-HLS-03-modules.md) [记录](baselines/hls-modules-validation.json) [记录](hls-validation.md)
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# 进度与证据
|
||||
|
||||
## 最新完成:PKG-AUDIO-03 源码拆分与生命周期修复
|
||||
|
||||
Audio Track 已拆为入口事件管理与独立媒体状态两个 JS 模块;修复原生暂停/结束后
|
||||
外部音频继续播放、销毁空 src 错误和旧引用复活,补失败回滚与自有订阅清理。
|
||||
公开 audio、同步 update、旧 URL/偏移/阈值语义保持,包内架构文档已同步。
|
||||
|
||||
44 项源码/89 项含三格式 Node 通过,CI 583 项;正式 main/legacy 三引擎各 42 项通过,
|
||||
其中保留 9 项旧缺陷观察和 3 项原生诊断。三份 docs/compiled 与包产物一致。
|
||||
首轮错误 currentSrc 断言及原始 trace 保留;main gzip 973(基线 676),不声称体积优化。
|
||||
当前 216 项、68 done、1 doing、147 todo。Audio-02 已提交 803e8725,03 独立提交核实后
|
||||
进入 04 严格 TS/公开类型;完整负偏移/缓冲、真机、分发门槛仍待后续,HLS 两风险仍 open。
|
||||
详见 [源码交付](changes/2026-09-12-PKG-AUDIO-03-lifecycle.md) 与 [冻结证据](baselines/audio-lifecycle-validation.json)。
|
||||
|
||||
## 最新完成:PKG-AUDIO-02 行为与真实媒体基线
|
||||
|
||||
38 项 Node 契约覆盖源码和发布三格式,完整 CI 577 项、254 个生产 TS 通过。
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
| HLS-ENV-01 | open / 已复现 | Windows WebKit 缺失 MSE,HLS 播放矩阵尚缺 Safari 证据 | PKG-HLS-05, REL-09, REVIEW-02 |
|
||||
| HLS-CRASH-01 | open / 已复现 | Firefox Hls 1.5.17 worker 销毁可在原生 video 与候选核心复现崩溃 | PKG-HLS-SDK-01, PKG-HLS-05, REVIEW-02 |
|
||||
| HLS-PLAYBACK-01 | open / 已复现 | Firefox 旧核心与 Hls 1.7.2 切组后视频档位未完成切换 | PKG-HLS-SDK-01, PKG-HLS-05, REVIEW-02 |
|
||||
| AUDIO-LIFE-01 | open / 已复现 | Audio Track 销毁后 update 与媒体/订阅缺少终止守卫 | PKG-AUDIO-02, PKG-AUDIO-03, PKG-AUDIO-05 |
|
||||
| AUDIO-LIFE-01 | resolved / 已复现 | Audio Track 销毁后 update 与媒体/订阅缺少终止守卫 | PKG-AUDIO-02, PKG-AUDIO-03, PKG-AUDIO-05 |
|
||||
| AUDIO-SYNC-01 | open / 已复现 | Audio Track 未同步原生暂停/结束,偏移边界与切源/缓冲仍待完整验证 | PKG-AUDIO-02, PKG-AUDIO-03, PKG-AUDIO-05 |
|
||||
| AUDIO-TYPE-01 | open / 源码/产物事实 | Audio Track update 部分配置与公开/编辑器声明不一致 | PKG-AUDIO-04 |
|
||||
| AUDIO-DEMO-01 | open / 源码/产物事实 | Audio Track README 示例链接名称不匹配且缺维护说明 | PKG-AUDIO-06 |
|
||||
+11
-4
@@ -2648,7 +2648,7 @@
|
||||
"id": "AUDIO-LIFE-01",
|
||||
"title": "Audio Track 销毁后 update 与媒体/订阅缺少终止守卫",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-AUDIO-02",
|
||||
"PKG-AUDIO-03",
|
||||
@@ -2662,7 +2662,13 @@
|
||||
"refactor/baselines/audio-validation.json"
|
||||
],
|
||||
"compatibleResolution": "保留公开 audio 和同步 update,封闭销毁后的资源写入并管理插件订阅,验证待决播放拒绝。",
|
||||
"closureCriteria": "受控复现和新旧核心真实媒体切源/销毁验证通过,不以源码观察代替泄漏证据。"
|
||||
"closureCriteria": "受控复现和新旧核心真实媒体切源/销毁验证通过,不以源码观察代替泄漏证据。",
|
||||
"resolutionEvidence": [
|
||||
"refactor/baselines/audio-lifecycle-validation.json",
|
||||
"refactor/changes/2026-09-12-PKG-AUDIO-03-lifecycle.md",
|
||||
"packages/artplayer-plugin-audio-track/ARCHITECTURE.md"
|
||||
],
|
||||
"resolutionRationale": "候选源码/三格式受控回归及 main/legacy 三引擎验证:插件自己解绑,关闭后的 update/回调/拒绝无资源写入,移除 src 后无新增媒体错误;安装/解绑失败仍释放。此关闭只覆盖已登记守卫/空 src 问题,不称全设备无内存泄漏。"
|
||||
},
|
||||
{
|
||||
"id": "AUDIO-SYNC-01",
|
||||
@@ -2679,9 +2685,10 @@
|
||||
"refactor/baselines/audio-track-release.json",
|
||||
"test/audio-track.test.js",
|
||||
"test/browser/audio-track.spec.js",
|
||||
"refactor/baselines/audio-validation.json"
|
||||
"refactor/baselines/audio-validation.json",
|
||||
"refactor/baselines/audio-lifecycle-validation.json"
|
||||
],
|
||||
"compatibleResolution": "固定严格阈值、偏移正负、倍率和音量语义;实测负目标/超时长及连续切源,按证据修复。",
|
||||
"compatibleResolution": "PKG-AUDIO-03 已修复原生暂停/结束与直接 seek 同步;严格阈值、偏移正负、倍率/音量不变。PKG-AUDIO-05 继续负目标/超时长、真实缓冲和连续切源的完整验证,条目仍 open。",
|
||||
"closureCriteria": "正常/边界/失败/缓冲恢复有媒体与受控断言,实际支持环境缺口明确处理。"
|
||||
},
|
||||
{
|
||||
|
||||
+5
-2
@@ -1511,11 +1511,14 @@
|
||||
"PKG-AUDIO-02",
|
||||
"CORE-10"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "外部 audio 的同步策略、监听与源更新生命周期",
|
||||
"acceptance": "结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-12-PKG-AUDIO-03-lifecycle.md",
|
||||
"baselines/audio-lifecycle-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-AUDIO-04",
|
||||
|
||||
+100
-1
@@ -136,7 +136,7 @@ for (const { name, factory } of implementations) {
|
||||
left.audio.calls.length = 0
|
||||
right.audio.calls.length = 0
|
||||
first.art.emit('destroy')
|
||||
assert.deepEqual(left.audio.calls, [['pause'], ['src', ''], ['load']])
|
||||
assert.deepEqual(left.audio.calls, [['pause'], name.startsWith('published') ? ['src', ''] : ['removeAttribute', 'src'], ['load']])
|
||||
assert.deepEqual(right.audio.calls, [])
|
||||
second.art.currentTime = 2
|
||||
second.art.emit('seek')
|
||||
@@ -145,6 +145,105 @@ for (const { name, factory } of implementations) {
|
||||
}
|
||||
|
||||
const historical = implementations.find(item => item.name === 'published-main').factory
|
||||
for (const { name, factory: candidate } of implementations.filter(item => !item.name.startsWith('published'))) {
|
||||
test(`${name}: closed callbacks and retained update cannot revive resources`, (t) => {
|
||||
const { art, listeners } = audioHost(t)
|
||||
const plugin = candidate({ url: 'audio.aac' })(art)
|
||||
const retained = [...listeners.values()].flatMap(set => [...set])
|
||||
art.emit('destroy')
|
||||
assert.equal([...listeners.values()].reduce((count, set) => count + set.size, 0), 0)
|
||||
plugin.audio.calls.length = 0
|
||||
Object.defineProperty(art, 'playing', { get() {
|
||||
throw new Error('Closed host read')
|
||||
} })
|
||||
assert.equal(plugin.update({ url: 'revived.aac' }), undefined)
|
||||
for (const callback of retained)
|
||||
callback()
|
||||
assert.deepEqual(plugin.audio.calls, [])
|
||||
})
|
||||
|
||||
test(`${name}: pending playback rejection becomes inert after destroy`, async (t) => {
|
||||
const { art, setPlay, warnings } = audioHost(t)
|
||||
let reject
|
||||
const pending = new Promise((resolve, fail) => {
|
||||
reject = fail
|
||||
})
|
||||
setPlay(() => pending)
|
||||
candidate({ url: 'audio.aac' })(art)
|
||||
art.emit('play').emit('destroy')
|
||||
reject(new Error('Cancelled playback'))
|
||||
await Promise.resolve()
|
||||
assert.deepEqual(warnings, [])
|
||||
})
|
||||
|
||||
test(`${name}: unsubscription failure does not prevent remaining cleanup`, (t) => {
|
||||
const { art } = audioHost(t)
|
||||
const plugin = candidate({ url: 'audio.aac' })(art)
|
||||
const off = art.off
|
||||
const failure = new Error('Controlled off failure')
|
||||
t.mock.method(art, 'off', (event, callback) => {
|
||||
if (event === 'play')
|
||||
throw failure
|
||||
return off(event, callback)
|
||||
})
|
||||
plugin.audio.calls.length = 0
|
||||
assert.throws(() => art.emit('destroy'), error => error === failure)
|
||||
assert.deepEqual(plugin.audio.calls, [['pause'], ['removeAttribute', 'src'], ['load']])
|
||||
plugin.audio.calls.length = 0
|
||||
art.emit('play')
|
||||
plugin.update({ url: 'revived.aac' })
|
||||
assert.deepEqual(plugin.audio.calls, [])
|
||||
})
|
||||
|
||||
test(`${name}: partial subscription failure releases the media and original error`, (t) => {
|
||||
const { art, instances, listeners } = audioHost(t)
|
||||
const on = art.on
|
||||
const failure = new Error('Controlled on failure')
|
||||
t.mock.method(art, 'on', (event, callback) => {
|
||||
on(event, callback)
|
||||
if (event === 'pause')
|
||||
throw failure
|
||||
return art
|
||||
})
|
||||
assert.throws(() => candidate({ url: 'audio.aac' })(art), error => error === failure)
|
||||
assert.deepEqual(instances[0].calls.slice(-3), [['pause'], ['removeAttribute', 'src'], ['load']])
|
||||
assert.equal([...listeners.values()].reduce((count, set) => count + set.size, 0), 0)
|
||||
})
|
||||
|
||||
test(`${name}: source initialization and pause failures still attempt media release`, (t) => {
|
||||
const failure = new Error('Controlled src failure')
|
||||
const { art, instances } = audioHost(t, (audio) => {
|
||||
Object.defineProperty(audio, 'src', {
|
||||
get: () => '',
|
||||
set() {
|
||||
throw failure
|
||||
},
|
||||
})
|
||||
audio.pause = () => {
|
||||
audio.calls.push(['pause'])
|
||||
throw new Error('Secondary cleanup failure')
|
||||
}
|
||||
})
|
||||
assert.throws(() => candidate({ url: 'audio.aac' })(art), error => error === failure)
|
||||
assert.deepEqual(instances[0].calls.slice(-3), [['pause'], ['removeAttribute', 'src'], ['load']])
|
||||
})
|
||||
|
||||
test(`${name}: native pause/end/seek synchronize audio without public pause calls`, (t) => {
|
||||
const { art } = audioHost(t)
|
||||
const { audio } = candidate({ url: 'audio.aac' })(art)
|
||||
audio.calls.length = 0
|
||||
art.emit('video:pause').emit('video:ended').emit('video:seeking')
|
||||
assert.deepEqual(audio.calls, [['pause'], ['pause'], ['pause']])
|
||||
art.currentTime = 3
|
||||
art.emit('video:seeked')
|
||||
assert.equal(audio.currentTime, 3)
|
||||
art.playing = true
|
||||
art.currentTime = 4
|
||||
art.emit('video:playing')
|
||||
assert.deepEqual(audio.calls.slice(-2), [['currentTime', 4], ['play']])
|
||||
})
|
||||
}
|
||||
|
||||
test('published defect AUDIO-LIFE-01: retained update reloads audio after destroy', (t) => {
|
||||
const { art, listeners } = audioHost(t)
|
||||
const plugin = historical({ url: 'audio.aac' })(art)
|
||||
|
||||
@@ -37,8 +37,8 @@ test.beforeAll(async () => {
|
||||
const bytes = readMember(await ensureArchive(release), member)
|
||||
assert.equal(hash(bytes), release.files[member])
|
||||
publishedCode = bytes.toString()
|
||||
sourceCode = await compilePackage(release.name, 'umd')
|
||||
evidence = { release, sourceSHA256: hash(sourceCode), audio: { sha256: hash(tone), bytes: tone.length, format: 'AAC mono 48000 Hz, 16 seconds, 440 Hz tone in MP4' }, wav: { sha256: hash(wav), bytes: wav.length, format: 'PCM16LE mono 16000 Hz, 16 seconds, integer square wave 200 Hz' }, scope: 'Source browser baseline; no physical device or installed package acceptance' }
|
||||
sourceCode = process.env.ARTPLAYER_AUDIO_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_AUDIO_ARTIFACT, 'utf8') : await compilePackage(release.name, 'umd')
|
||||
evidence = { release, sourceSHA256: hash(sourceCode), candidate: process.env.ARTPLAYER_AUDIO_ARTIFACT || 'workspace source build', audio: { sha256: hash(tone), bytes: tone.length, format: 'AAC mono 48000 Hz, 16 seconds, 440 Hz tone in MP4' }, wav: { sha256: hash(wav), bytes: wav.length, format: 'PCM16LE mono 16000 Hz, 16 seconds, integer square wave 200 Hz' }, scope: 'Source or explicit artifact browser checks; no physical device or installed package acceptance' }
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
@@ -116,6 +116,31 @@ test('published AUDIO-LIFE-01: empty src cleanup produces a native media error',
|
||||
})
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core} core + source audio: native pause/end stop audio and closed references stay inert`, async ({ page }, testInfo) => {
|
||||
await openAudio(page, core, 'source', testInfo)
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentTime)).toBeGreaterThan(0.2)
|
||||
await page.evaluate(() => window.art.video.pause())
|
||||
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.paused)).toBe(true)
|
||||
await page.evaluate(() => {
|
||||
window.art.video.currentTime = 4
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentTime)).toBeCloseTo(4, 1)
|
||||
await page.evaluate(() => {
|
||||
window.art.seek = 7.8
|
||||
window.art.play()
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.video.ended)).toBe(true)
|
||||
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.paused)).toBe(true)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.networkState)).toBe(0)
|
||||
expect(await page.evaluate(() => window.audioPlugin.audio.readyState)).toBe(0)
|
||||
expect(await page.evaluate(() => window.audioPlugin.audio.error)).toBeNull()
|
||||
await page.evaluate(() => window.audioPlugin.update({ url: '/test/audio-tone.m4a?source=revived' }))
|
||||
expect(await page.evaluate(() => window.audioPlugin.audio.getAttribute('src'))).toBeNull()
|
||||
expect(await page.evaluate(() => window.audioPlugin.audio.networkState)).toBe(0)
|
||||
})
|
||||
|
||||
test(`${core} core + published audio: native pause and end leave external audio playing`, async ({ page }, testInfo) => {
|
||||
await openAudio(page, core, 'published', testInfo)
|
||||
await page.locator('#play').click()
|
||||
@@ -181,7 +206,7 @@ for (const core of ['published', 'candidate']) {
|
||||
window.art.destroy()
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.readyState)).toBe(0)
|
||||
expect(await page.evaluate(() => window.audioPlugin.audio.getAttribute('src'))).toBe('')
|
||||
expect(await page.evaluate(() => window.audioPlugin.audio.getAttribute('src'))).toBe(plugin === 'published' ? '' : null)
|
||||
expect(await page.evaluate(() => window.audioPlugin.audio.paused)).toBe(true)
|
||||
expect(await page.locator('.art-video-player').count()).toBe(0)
|
||||
})
|
||||
@@ -200,7 +225,7 @@ for (const core of ['published', 'candidate']) {
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.readyState)).toBe(0)
|
||||
expect(await page.evaluate(() => window.audioPlugin.audio.paused)).toBe(true)
|
||||
expect(await page.evaluate(() => window.audioPlugin.audio.getAttribute('src'))).toBe('')
|
||||
expect(await page.evaluate(() => window.audioPlugin.audio.getAttribute('src'))).toBe(plugin === 'published' ? '' : null)
|
||||
})
|
||||
}
|
||||
}
|
||||
+31
-20
@@ -1,10 +1,27 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { Buffer } from 'node:buffer'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import vm from 'node:vm'
|
||||
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
|
||||
import { loadPackage } from './load.js'
|
||||
|
||||
async function readFactory(bytes, esm) {
|
||||
if (esm)
|
||||
return (await import(`data:text/javascript;base64,${Buffer.from(bytes).toString('base64')}`)).default
|
||||
const context = {
|
||||
module: { exports: {} },
|
||||
Audio: function Audio() {
|
||||
return new globalThis.Audio()
|
||||
},
|
||||
console,
|
||||
}
|
||||
context.exports = context.module.exports
|
||||
vm.runInNewContext(bytes.toString(), context, { timeout: 5000 })
|
||||
return context.module.exports
|
||||
}
|
||||
|
||||
export async function audioImplementations() {
|
||||
const { release } = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/audio-track-release.json', import.meta.url)))
|
||||
const archive = await ensureArchive(release)
|
||||
@@ -13,29 +30,16 @@ export async function audioImplementations() {
|
||||
const member = `package/${release.manifest[format].replace(/^\.\//, '')}`
|
||||
const bytes = readMember(archive, member)
|
||||
assert.equal(hash(bytes), release.files[member])
|
||||
let factory
|
||||
if (format === 'module') {
|
||||
factory = (await import(`data:text/javascript;base64,${Buffer.from(bytes).toString('base64')}`)).default
|
||||
}
|
||||
else {
|
||||
const context = {
|
||||
module: { exports: {} },
|
||||
Audio: function Audio() {
|
||||
return new globalThis.Audio()
|
||||
},
|
||||
console,
|
||||
}
|
||||
context.exports = context.module.exports
|
||||
vm.runInNewContext(bytes.toString(), context, { timeout: 5000 })
|
||||
factory = context.module.exports
|
||||
}
|
||||
const factory = await readFactory(bytes, format === 'module')
|
||||
implementations.push({ name: `published-${format}`, factory })
|
||||
}
|
||||
for (const file of (process.env.ARTPLAYER_TEST_AUDIO || '').split(path.delimiter).filter(Boolean))
|
||||
implementations.push({ name: `artifact-${path.basename(file)}`, factory: await readFactory(fs.readFileSync(file), file.endsWith('.mjs')) })
|
||||
return implementations
|
||||
}
|
||||
|
||||
// Records plugin intent only. It does not decode media or emulate browser timing.
|
||||
export function audioHost(t) {
|
||||
export function audioHost(t, initialize = () => {}) {
|
||||
const instances = []
|
||||
const warnings = []
|
||||
let play = () => Promise.resolve()
|
||||
@@ -43,17 +47,20 @@ export function audioHost(t) {
|
||||
constructor() {
|
||||
instances.push(this)
|
||||
this.calls = []
|
||||
this.values = new Map()
|
||||
this.paused = true
|
||||
for (const [name, initial] of Object.entries({ src: '', preload: '', currentTime: 0, playbackRate: 1, volume: 1, muted: false })) {
|
||||
let value = initial
|
||||
this.values.set(name, initial)
|
||||
Object.defineProperty(this, name, {
|
||||
get: () => value,
|
||||
configurable: true,
|
||||
get: () => this.values.get(name),
|
||||
set: (next) => {
|
||||
this.calls.push([name, next])
|
||||
value = next
|
||||
this.values.set(name, next)
|
||||
},
|
||||
})
|
||||
}
|
||||
initialize(this)
|
||||
}
|
||||
|
||||
play() {
|
||||
@@ -63,6 +70,10 @@ export function audioHost(t) {
|
||||
|
||||
pause() { this.calls.push(['pause']) }
|
||||
load() { this.calls.push(['load']) }
|
||||
removeAttribute(name) {
|
||||
this.calls.push(['removeAttribute', name])
|
||||
this.values.set(name, '')
|
||||
}
|
||||
}
|
||||
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'Audio')
|
||||
Object.defineProperty(globalThis, 'Audio', { configurable: true, value: Audio })
|
||||
|
||||
Reference in new issue
Block a user