mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-28 10:31:20 -08:00
fix: silence unhandled play() rejections from internal callers
`art.play()` is an async wrapper around the native `video.play()` promise, which rejects when a pending play request is interrupted (`pause()`, `load()`, a new `src`, or the element being removed) and when autoplay is blocked. Every internal caller fires it from an event handler and cannot await it, so the rejection escapes as a global `unhandledrejection`. Error trackers report it as an uncaught crash even though the player recovers on its own — for example destroying the player right after mount, which is routine when the video sits in a modal the user can close quickly. Add a `silencePromise` util (the same approach video.js uses) and apply it to the non-awaited internal call sites: mask, play control, mini player, loop restart, autoPlayback jump, hotkey and click toggles. `switchMix` is fixed too, for a second symptom: `await art.play()` inside the async `video:canplay` handler both leaked the rejection and left the `switchUrl` promise pending forever, since the throw never reached the enclosing executor's `reject`. It now settles. The public API is unchanged: `art.play()` and `art.toggle()` still return the original promise, so applications that call them keep their own rejection to handle. Only the internal, non-awaited copy is silenced.
This commit is contained in:
1 parent
0b9c31a8cf
commit
0f47e11d63
10 files changed
+44
-18
No files matched your search
@@ -1,4 +1,4 @@
|
|||||||
import { append, setStyle, tooltip } from '../utils'
|
import { append, setStyle, silencePromise, tooltip } from '../utils'
|
||||||
|
|
||||||
export default function playAndPause(option) {
|
export default function playAndPause(option) {
|
||||||
return art => ({
|
return art => ({
|
||||||
@@ -12,7 +12,7 @@ export default function playAndPause(option) {
|
|||||||
tooltip($pause, i18n.get('Pause'))
|
tooltip($pause, i18n.get('Pause'))
|
||||||
|
|
||||||
proxy($play, 'click', () => {
|
proxy($play, 'click', () => {
|
||||||
art.play()
|
silencePromise(art.play())
|
||||||
})
|
})
|
||||||
|
|
||||||
proxy($pause, 'click', () => {
|
proxy($pause, 'click', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { includeFromEvent, isMobile } from '../utils'
|
import { includeFromEvent, isMobile, silencePromise } from '../utils'
|
||||||
|
|
||||||
export default function clickInit(art, events) {
|
export default function clickInit(art, events) {
|
||||||
const {
|
const {
|
||||||
@@ -35,11 +35,11 @@ export default function clickInit(art, events) {
|
|||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
if (!art.isLock && MOBILE_CLICK_PLAY) {
|
if (!art.isLock && MOBILE_CLICK_PLAY) {
|
||||||
art.toggle()
|
silencePromise(art.toggle())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
art.toggle()
|
silencePromise(art.toggle())
|
||||||
}
|
}
|
||||||
clickTimes = clicks
|
clickTimes = clicks
|
||||||
break
|
break
|
||||||
@@ -48,7 +48,7 @@ export default function clickInit(art, events) {
|
|||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
if (!art.isLock && MOBILE_DBCLICK_PLAY) {
|
if (!art.isLock && MOBILE_DBCLICK_PLAY) {
|
||||||
art.toggle()
|
silencePromise(art.toggle())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isMobile } from './utils'
|
import { isMobile, silencePromise } from './utils'
|
||||||
|
|
||||||
export default class Hotkey {
|
export default class Hotkey {
|
||||||
constructor(art) {
|
constructor(art) {
|
||||||
@@ -21,7 +21,7 @@ export default class Hotkey {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.add('Space', () => {
|
this.add('Space', () => {
|
||||||
this.art.toggle()
|
silencePromise(this.art.toggle())
|
||||||
})
|
})
|
||||||
|
|
||||||
this.add('ArrowLeft', () => {
|
this.add('ArrowLeft', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { append, setStyle } from './utils'
|
import { append, setStyle, silencePromise } from './utils'
|
||||||
import Component from './utils/component'
|
import Component from './utils/component'
|
||||||
|
|
||||||
export default class Mask extends Component {
|
export default class Mask extends Component {
|
||||||
@@ -18,6 +18,6 @@ export default class Mask extends Component {
|
|||||||
setStyle($error, 'display', null)
|
setStyle($error, 'display', null)
|
||||||
})
|
})
|
||||||
|
|
||||||
events.proxy(template.$state, 'click', () => art.play())
|
events.proxy(template.$state, 'click', () => silencePromise(art.play()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import config from '../config'
|
import config from '../config'
|
||||||
import { addClass, isMobile, setStyle, sleep } from '../utils'
|
import { addClass, isMobile, setStyle, silencePromise, sleep } from '../utils'
|
||||||
|
|
||||||
export default function eventInit(art) {
|
export default function eventInit(art) {
|
||||||
const {
|
const {
|
||||||
@@ -51,7 +51,7 @@ export default function eventInit(art) {
|
|||||||
art.on('video:ended', () => {
|
art.on('video:ended', () => {
|
||||||
if (option.loop) {
|
if (option.loop) {
|
||||||
art.seek = 0
|
art.seek = 0
|
||||||
art.play()
|
silencePromise(art.play())
|
||||||
art.controls.show = false
|
art.controls.show = false
|
||||||
art.mask.show = false
|
art.mask.show = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { addClass, append, createElement, def, getRect, hasClass, isInViewport, removeClass, setStyle } from '../utils'
|
import { addClass, append, createElement, def, getRect, hasClass, isInViewport, removeClass, setStyle, silencePromise } from '../utils'
|
||||||
|
|
||||||
export default function miniMix(art) {
|
export default function miniMix(art) {
|
||||||
const {
|
const {
|
||||||
@@ -53,7 +53,7 @@ export default function miniMix(art) {
|
|||||||
const $state = append($mini, `<div class="art-mini-state"></div>`)
|
const $state = append($mini, `<div class="art-mini-state"></div>`)
|
||||||
const $play = append($state, icons.play)
|
const $play = append($state, icons.play)
|
||||||
const $pause = append($state, icons.pause)
|
const $pause = append($state, icons.pause)
|
||||||
proxy($play, 'click', () => art.play())
|
proxy($play, 'click', () => silencePromise(art.play()))
|
||||||
proxy($pause, 'click', () => art.pause())
|
proxy($pause, 'click', () => art.pause())
|
||||||
initState($play, $pause)
|
initState($play, $pause)
|
||||||
art.on('video:playing', () => initState($play, $pause))
|
art.on('video:playing', () => initState($play, $pause))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { def } from '../utils'
|
import { def, silencePromise } from '../utils'
|
||||||
|
|
||||||
export default function switchMix(art) {
|
export default function switchMix(art) {
|
||||||
function switchUrl(url, currentTime) {
|
function switchUrl(url, currentTime) {
|
||||||
@@ -31,7 +31,7 @@ export default function switchMix(art) {
|
|||||||
art.aspectRatio = aspectRatio
|
art.aspectRatio = aspectRatio
|
||||||
|
|
||||||
if (playing) {
|
if (playing) {
|
||||||
await art.play()
|
await silencePromise(art.play())
|
||||||
}
|
}
|
||||||
|
|
||||||
art.notice.show = ''
|
art.notice.show = ''
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { append, query, secondToTime, setStyle } from '../utils'
|
import { append, query, secondToTime, setStyle, silencePromise } from '../utils'
|
||||||
|
|
||||||
export default function autoPlayback(art) {
|
export default function autoPlayback(art) {
|
||||||
const {
|
const {
|
||||||
@@ -57,7 +57,7 @@ export default function autoPlayback(art) {
|
|||||||
|
|
||||||
proxy($jump, 'click', () => {
|
proxy($jump, 'click', () => {
|
||||||
art.seek = currentTime
|
art.seek = currentTime
|
||||||
art.play()
|
silencePromise(art.play())
|
||||||
setStyle($poster, 'display', 'none')
|
setStyle($poster, 'display', 'none')
|
||||||
setStyle($autoPlayback, 'display', 'none')
|
setStyle($autoPlayback, 'display', 'none')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,3 +14,28 @@ export function errorHandle(condition, msg) {
|
|||||||
}
|
}
|
||||||
return condition
|
return condition
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach a no-op catch to a promise that is intentionally not awaited.
|
||||||
|
*
|
||||||
|
* `art.play()` resolves the native `video.play()` promise, which rejects when the
|
||||||
|
* request is interrupted (a `pause()`, a `load()`, a new `src`, or the element
|
||||||
|
* being removed while the request is still pending) or when autoplay is blocked.
|
||||||
|
* Internal call sites fire it from event handlers and cannot await it, so the
|
||||||
|
* rejection escapes as a global `unhandledrejection` and error trackers report it
|
||||||
|
* as a crash even though the player recovers on its own.
|
||||||
|
*
|
||||||
|
* Nothing is hidden from users of the public API: `art.play()` and `art.toggle()`
|
||||||
|
* still return the original promise, so an application that calls them keeps its
|
||||||
|
* own rejection to handle. Only the internal, non-awaited copy is silenced.
|
||||||
|
*
|
||||||
|
* Returns a promise that always fulfills, so an internal caller may await it
|
||||||
|
* without inheriting the rejection. A non-thenable value is returned untouched
|
||||||
|
* (`art.toggle()` yields `art.pause()`, which is synchronous).
|
||||||
|
*/
|
||||||
|
export function silencePromise(value) {
|
||||||
|
if (value && typeof value.catch === 'function') {
|
||||||
|
return value.catch(() => {})
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
Vendored
+1
@@ -43,6 +43,7 @@ export interface Utils {
|
|||||||
loadImg: (url: string, scale?: number) => Promise<HTMLImageElement>
|
loadImg: (url: string, scale?: number) => Promise<HTMLImageElement>
|
||||||
|
|
||||||
errorHandle: <T extends boolean>(condition: T, msg: string) => T extends true ? T : never
|
errorHandle: <T extends boolean>(condition: T, msg: string) => T extends true ? T : never
|
||||||
|
silencePromise: <T>(value: T) => T extends PromiseLike<infer R> ? Promise<R | undefined> : T
|
||||||
def: (obj: object, name: string, value: unknown) => void
|
def: (obj: object, name: string, value: unknown) => void
|
||||||
has: (obj: object, name: PropertyKey) => boolean
|
has: (obj: object, name: PropertyKey) => boolean
|
||||||
get: (obj: object, name: PropertyKey) => PropertyDescriptor | undefined
|
get: (obj: object, name: PropertyKey) => PropertyDescriptor | undefined
|
||||||
|
|||||||
Reference in new issue
Block a user