hello. happy Friday or near Friday wherever you are. Here is a quick component for capturing media in niantic studio. This component can take a screenshot, record video and share. It is straight css; so no extra dependencies. There are likely bugs as I wrote this last night
. Hopefully this is a good starting point for you!
import * as ecs from '@8thwall/ecs'
ecs.registerComponent({
name: 'mediaCapture',
// schema: {
// },
// schemaDefaults: {
// },
// data: {
// },
add: (world, component) => {
// create circular button
const style = document.createElement('style')
style.textContent = `
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.6; }
100% { transform: scale(1); opacity: 1; }
}
.pulsing {
animation: pulse 1.2s infinite ease-in-out;
}
`
document.head.appendChild(style)
const wrapper = document.createElement('div')
wrapper.style.position = 'fixed'
wrapper.style.bottom = '8rem'
wrapper.style.left = '50%'
wrapper.style.transform = 'translateX(-50%)'
wrapper.style.width = '60px'
wrapper.style.height = '60px'
wrapper.style.display = 'flex'
wrapper.style.justifyContent = 'center'
wrapper.style.alignItems = 'center'
wrapper.style.zIndex = '9'
wrapper.id = 'capture-wrapper'
// outer ring
const outerRing = document.createElement('div')
outerRing.id = 'capture-ring'
outerRing.style.position = 'absolute'
outerRing.style.width = '60px'
outerRing.style.height = '60px'
outerRing.style.borderRadius = '50%'
outerRing.style.border = '3px solid white'
outerRing.style.boxSizing = 'border-box'
outerRing.style.pointerEvents = 'none'
wrapper.appendChild(outerRing)
const btn = document.createElement('button')
btn.id = 'capture-btn'
btn.style.width = '52px'
btn.style.height = '52px'
btn.style.borderRadius = '50%'
btn.style.background = '#fff'
btn.style.border = '2px solid white'
btn.style.cursor = 'pointer'
btn.style.outline = 'none'
wrapper.appendChild(btn)
document.body.appendChild(wrapper)
let recording = false
let pressTimer = null
const HOLD_THRESHOLD = 300
const start = () => {
pressTimer = setTimeout(() => {
// Long press → start video recording
if (!recording) {
console.log('Start recording…')
world.xr.startMediaRecorder()
recording = true
outerRing.classList.add('pulsing')
btn.style.background = '#ff0000'
}
}, HOLD_THRESHOLD)
}
const stop = () => {
clearTimeout(pressTimer)
if (recording) {
// End long-press → stop video recording
console.log('Stop recording…')
world.xr.stopMediaRecorder()
recording = false
outerRing.classList.remove('pulsing')
btn.style.background = '#fff'
} else {
// Short tap → take screenshot
console.log('Taking screenshot…')
world.xr.takeScreenshot()
}
}
// mobile touch only
btn.addEventListener('touchstart', (e) => {
e.preventDefault()
start()
})
btn.addEventListener('touchend', (e) => {
e.preventDefault()
stop()
})
btn.addEventListener('touchcancel', (e) => {
e.preventDefault()
clearTimeout(pressTimer)
if (recording) {
console.log('Stop recording (cancel)…')
world.xr.stopRecording()
recording = false
outerRing.classList.remove('pulsing')
btn.style.background = '#fff'
}
})
world.events.addListener(world.events.globalId, ecs.events.RECORDER_VIDEO_ERROR, (error) => {
console.error('Recorder error:', error.message)
})
world.events.addListener(
world.events.globalId,
ecs.events.RECORDER_SCREENSHOT_READY,
(e) => {
console.log('Screenshot ready:', e)
showPreviewOverlay({type: 'image', blob: e.data})
}
)
world.events.addListener(
world.events.globalId,
ecs.events.RECORDER_VIDEO_READY,
(e) => {
showPreviewOverlay({type: 'video', blob: e.data.videoBlob})
}
)
},
})
function showPreviewOverlay({type, blob}) {
const url = URL.createObjectURL(blob)
const overlay = document.createElement('div')
overlay.style.position = 'fixed'
overlay.style.top = '0'
overlay.style.left = '0'
overlay.style.width = '100%'
overlay.style.height = '100%'
overlay.style.background = 'rgba(0,0,0,0.6)'
overlay.style.backdropFilter = 'blur(8px)'
overlay.style.display = 'flex'
overlay.style.flexDirection = 'column'
overlay.style.justifyContent = 'center'
overlay.style.alignItems = 'center'
overlay.style.zIndex = '1000'
// Close button
const closeBtn = document.createElement('button')
closeBtn.textContent = '✕'
closeBtn.style.position = 'absolute'
closeBtn.style.top = '2rem'
closeBtn.style.right = '2rem'
closeBtn.style.background = 'rgba(0,0,0,0.5)'
closeBtn.style.color = 'white'
closeBtn.style.fontSize = '1.25rem'
closeBtn.style.border = 'none'
closeBtn.style.borderRadius = '50%'
closeBtn.style.width = '40px'
closeBtn.style.height = '40px'
closeBtn.style.cursor = 'pointer'
closeBtn.addEventListener('click', () => overlay.remove())
overlay.appendChild(closeBtn)
let file
if (type === 'video') {
const vid = document.createElement('video')
vid.src = url
vid.autoplay = true
vid.loop = true
vid.playsInline = true
vid.controls = true
vid.muted = true
vid.style.width = '230px'
vid.style.height = 'auto'
vid.style.borderRadius = '12px'
vid.style.border = '3px solid white'
overlay.appendChild(vid)
file = new File([blob], 'recording.mp4', {type: 'video/mp4'})
} else if (type === 'image') {
const img = document.createElement('img')
img.src = url
img.style.width = '230px'
img.style.height = 'auto'
img.style.borderRadius = '12px'
img.style.border = '3px solid white'
overlay.appendChild(img)
file = new File([blob], 'screenshot.jpg', {type: 'image/jpeg'})
}
// share button
const shareBtn = document.createElement('button')
shareBtn.textContent = '📤 Share'
shareBtn.style.position = 'absolute'
shareBtn.style.bottom = '2rem'
shareBtn.style.left = '50%'
shareBtn.style.transform = 'translateX(-50%)'
shareBtn.style.padding = '0.8rem 2rem'
shareBtn.style.fontSize = '1rem'
shareBtn.style.fontWeight = '600'
shareBtn.style.background = '#e40089'
shareBtn.style.color = 'white'
shareBtn.style.border = 'none'
shareBtn.style.fontFamily = 'Futura'
shareBtn.style.letterSpacing = '1px'
shareBtn.style.borderRadius = '9999px'
shareBtn.style.cursor = 'pointer'
shareBtn.addEventListener('click', async () => {
try {
await navigator.share({files: [file]})
console.log('Shared successfully')
} catch (err) {
console.warn('Share failed:', err)
}
})
overlay.appendChild(shareBtn)
document.body.appendChild(overlay)
}