fix(audio): [PKG-AUDIO-05] checkpoint zero-time resume and native buffering evidence

This commit is contained in:
Harvey Zhao committed 2026-09-12 03:58:39 +08:00
1 parent 0218196df9
commit d18c545675
25 files changed
+14373 -32

No files matched your search

@@ -26,6 +26,13 @@ pause audio; seeked synchronizes and resumes only if the video is playing. Nativ
synchronizes again before resume. Ordinary playing timeupdate retains the strict drift
threshold and offset rule. Rate and volume/mute follow the existing core properties.
The entry owns 15 subscriptions. Resume checks respect an explicit proxy video.playing
boolean first. For native media they also accept an unpaused, non-ended video with
readyState > 2 at currentTime zero: the historical art.playing getter requires time > 0,
which can otherwise leave audio paused after the seek associated with a source switch.
If seeked arrives before readiness, canplay synchronizes and resumes only paused audio
whose host is playing. A paused host or proxy playing=false must never trigger recovery.
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.
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!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)}}}}});
!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(){const e=t.video;return!!e&&("playing"in e&&"boolean"===typeof e.playing?e.playing:t.playing||!1===e.paused&&!e.ended&&e.readyState>2)}function r(e,o){const n=()=>{i&&o()};a.push([e,n]),t.on(e,n)}function d(){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{r("play",()=>{u(),o.play()});for(const e of["pause","video:pause","video:ended","video:waiting","video:emptied","video:seeking"])r(e,o.pause);r("seek",u),r("video:seeked",()=>{u(),c()&&o.play()}),r("video:timeupdate",()=>{t.playing&&u()}),r("video:ratechange",()=>{n.playbackRate=t.video.playbackRate}),r("video:volumechange",()=>{n.volume=t.volume,n.muted=t.muted}),r("video:playing",()=>{c()&&(u(),o.play())}),r("video:canplay",()=>{n.paused&&c()&&(u(),o.play())}),r("destroy",d),n.volume=t.volume,n.muted=t.muted,n.playbackRate=t.video?.playbackRate||1}catch(s){try{d()}catch{}throw s}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,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)}}}}});
!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(){const e=t.video;return!!e&&("playing"in e&&"boolean"===typeof e.playing?e.playing:t.playing||!1===e.paused&&!e.ended&&e.readyState>2)}function d(e,o){const n=()=>{u&&o()};i.push([e,n]),t.on(e,n)}function l(){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{d("play",()=>{c(),n.play()});for(const e of["pause","video:pause","video:ended","video:waiting","video:emptied","video:seeking"])d(e,n.pause);d("seek",c),d("video:seeked",()=>{c(),r()&&n.play()}),d("video:timeupdate",()=>{t.playing&&c()}),d("video:ratechange",()=>{a.playbackRate=t.video.playbackRate}),d("video:volumechange",()=>{a.volume=t.volume,a.muted=t.muted}),d("video:playing",()=>{r()&&(c(),n.play())}),d("video:canplay",()=>{a.paused&&r()&&(c(),n.play())}),d("destroy",l),a.volume=t.volume,a.muted=t.muted,a.playbackRate=(null==(o=t.video)?void 0:o.playbackRate)||1}catch(s){try{l()}catch(p){}throw s}return{name:"artplayerPluginAudioTrack",audio:a,update(e){u&&n.update(e,t.playing)}}}}});
@@ -84,6 +84,14 @@ function artplayerPluginAudioTrack(option) {
if (art.video)
track.sync(art.currentTime);
}
function canResumeAudio() {
const video = art.video;
if (!video)
return false;
if ("playing" in video && typeof video.playing === "boolean")
return video.playing;
return art.playing || video.paused === false && !video.ended && video.readyState > 2;
}
function listen(event, callback) {
const listener = () => {
if (active)
@@ -122,7 +130,7 @@ function artplayerPluginAudioTrack(option) {
listen("seek", syncAudio);
listen("video:seeked", () => {
syncAudio();
if (art.playing)
if (canResumeAudio())
track.play();
});
listen("video:timeupdate", () => {
@@ -137,7 +145,13 @@ function artplayerPluginAudioTrack(option) {
audio.muted = art.muted;
});
listen("video:playing", () => {
if (art.playing) {
if (canResumeAudio()) {
syncAudio();
track.play();
}
});
listen("video:canplay", () => {
if (audio.paused && canResumeAudio()) {
syncAudio();
track.play();
}
@@ -2,7 +2,7 @@ import type Artplayer from 'artplayer'
import type { Option, RuntimeResult, UpdateOption } from '../types/artplayer-plugin-audio-track'
import { createAudioTrack } from './track'
type AudioEvent = 'play' | 'pause' | 'seek' | 'destroy' | 'video:pause' | 'video:ended' | 'video:waiting' | 'video:emptied' | 'video:seeking' | 'video:seeked' | 'video:timeupdate' | 'video:ratechange' | 'video:volumechange' | 'video:playing'
type AudioEvent = 'play' | 'pause' | 'seek' | 'destroy' | 'video:pause' | 'video:ended' | 'video:waiting' | 'video:emptied' | 'video:seeking' | 'video:seeked' | 'video:timeupdate' | 'video:ratechange' | 'video:volumechange' | 'video:playing' | 'video:canplay'
export default function artplayerPluginAudioTrack(option: Option) {
return (art: Artplayer): RuntimeResult => {
@@ -16,6 +16,15 @@ export default function artplayerPluginAudioTrack(option: Option) {
track.sync(art.currentTime)
}
function canResumeAudio() {
const video = art.video
if (!video)
return false
if ('playing' in video && typeof video.playing === 'boolean')
return video.playing
return art.playing || (video.paused === false && !video.ended && video.readyState > 2)
}
function listen(event: AudioEvent, callback: () => void) {
const listener = () => {
if (active)
@@ -54,7 +63,7 @@ export default function artplayerPluginAudioTrack(option: Option) {
listen('seek', syncAudio)
listen('video:seeked', () => {
syncAudio()
if (art.playing)
if (canResumeAudio())
track.play()
})
listen('video:timeupdate', () => {
@@ -69,7 +78,13 @@ export default function artplayerPluginAudioTrack(option: Option) {
audio.muted = art.muted
})
listen('video:playing', () => {
if (art.playing) {
if (canResumeAudio()) {
syncAudio()
track.play()
}
})
listen('video:canplay', () => {
if (audio.paused && canResumeAudio()) {
syncAudio()
track.play()
}