Skip to content

Commit e094b06

Browse files
committed
feat(capture): render <audio controls> as a representative player
The native <audio controls> UI is a UA shadow-DOM widget that can't be serialized, so it captured blank. Draw a representative player (rounded bar, play triangle, timeline with knob, time label) sized to the element, mirroring the existing <video> special case. Only when the element has `controls`; without it the native element is display:none, left to the generic (invisible) clone. Closes #444
1 parent aa1afc4 commit e094b06

2 files changed

Lines changed: 53 additions & 0 deletions

File tree

__tests__/core.clone.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,24 @@ describe('deepClone', () => {
3737
expect(clone.src.startsWith('data:image/')).toBe(true)
3838
})
3939

40+
it('renders <audio controls> as a player image (#444)', async () => {
41+
const audio = document.createElement('audio')
42+
audio.controls = true
43+
document.body.appendChild(audio)
44+
const clone = await runClone(audio)
45+
document.body.removeChild(audio)
46+
expect(clone.tagName).toBe('IMG')
47+
expect(clone.src.startsWith('data:image/svg+xml')).toBe(true)
48+
})
49+
50+
it('leaves <audio> without controls to the generic clone (#444)', async () => {
51+
const audio = document.createElement('audio')
52+
document.body.appendChild(audio)
53+
const clone = await runClone(audio)
54+
document.body.removeChild(audio)
55+
expect(clone.tagName).toBe('AUDIO')
56+
})
57+
4058
it('deepClone handles data-capture="exclude"', async () => {
4159
const el = document.createElement('div')
4260
el.setAttribute('data-capture', 'exclude')

src/core/clone.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,41 @@ export async function deepClone(node, sessionCache, options) {
248248
return img
249249
}
250250

251+
if (node.tagName === 'AUDIO' && node.controls) {
252+
// The native <audio controls> UI is a UA shadow-DOM widget that can't be
253+
// serialized, so a plain clone renders blank. Draw a representative player
254+
// sized to the element (#444). Without `controls` the native element is
255+
// display:none, so we leave those to the generic (invisible) clone.
256+
const { width, height } = getUnscaledDimensions(node)
257+
const w = Math.round(width || node.offsetWidth || 300)
258+
const h = Math.round(height || node.offsetHeight || 54)
259+
const cy = h / 2
260+
const tri = Math.max(4, h * 0.16)
261+
const px = h * 0.34
262+
const rTime = w - h * 0.34
263+
const trackX = px + tri + h * 0.55
264+
const trackW = Math.max(0, rTime - h * 0.7 - trackX)
265+
const fs = Math.max(9, Math.round(h * 0.24))
266+
const svg =
267+
`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">` +
268+
`<rect width="${w}" height="${h}" rx="${Math.min(h / 2, 10)}" fill="#f1f3f4"/>` +
269+
`<path d="M ${px} ${cy - tri} L ${px + tri} ${cy} L ${px} ${cy + tri} Z" fill="#5f6368"/>` +
270+
`<rect x="${trackX}" y="${cy - 1.5}" width="${trackW}" height="3" rx="1.5" fill="#bdc1c6"/>` +
271+
`<circle cx="${trackX}" cy="${cy}" r="${Math.max(3, h * 0.09)}" fill="#5f6368"/>` +
272+
`<text x="${rTime}" y="${cy}" fill="#5f6368" font-family="sans-serif" font-size="${fs}" text-anchor="end" dominant-baseline="central">0:00</text>` +
273+
'</svg>'
274+
const img = document.createElement('img')
275+
try { img.decoding = 'sync'; img.loading = 'eager' } catch {}
276+
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
277+
img.width = w
278+
img.height = h
279+
img.style.width = `${w}px`
280+
img.style.height = `${h}px`
281+
sessionCache.nodeMap.set(img, node)
282+
inlineAllStyles(node, img, sessionCache, options)
283+
return img
284+
}
285+
251286
let clone
252287
try {
253288
clone = node.cloneNode(false)

0 commit comments

Comments
 (0)