If I wanted to Add and Customize Video Recording/ Sharing to Niantic Studio Project do I need to clone the entire XRExtras repository inside my project or can I only use a specific portion of it?
The XRExtras MediaRecorder Repo
If I wanted to Add and Customize Video Recording/ Sharing to Niantic Studio Project do I need to clone the entire XRExtras repository inside my project or can I only use a specific portion of it?
The XRExtras MediaRecorder Repo
XRExtras is not compatible with Studio, youβll have to build it out yourself. However weβre working on making these features apart of the sample project(s).
When could I look forward to have that functionality in the sample projects? Just wondering if I should spend my time figuring it out now or I could wait a bit, Thanks!
I would say go for it! Weβre still working on adding the functionality and thereβs no definitive date I can give.
Ok so itβs not perfect but Iβm capturing the screenshot and letting the user share it natively. Would be great to get some feedback cause Iβm probably not doing this optimally
import * as ecs from '@8thwall/ecs' // This is how you access the ecs library.
import {shareImage} from './share'
declare const XR8: any
ecs.registerComponent({
name: 'screenshot-capture',
schema: {
overlayFrame: ecs.eid,
topFrame: ecs.eid,
previewFrame: ecs.eid,
previewImage: ecs.eid,
previewCloseButton: ecs.eid,
captureButton: ecs.eid,
shareButton: ecs.eid,
},
// schemaDefaults: {
// button: 'screenshotBtn',
// previewImage: 'screenshotPreview',
// },
data: {
imageData: ecs.string, // Base64 image data
},
// add: (world, component) => {
// },
// tick: (world, component) => {
// },
// remove: (world, component) => {
// },
stateMachine: ({world, eid, schemaAttribute, dataAttribute}) => {
const {
overlayFrame,
topFrame,
previewFrame,
captureButton,
previewImage,
previewCloseButton,
shareButton,
} = schemaAttribute.get(eid)
const data = dataAttribute.cursor(eid)
const toPreview = ecs.defineTrigger()
const toIdle = ecs.defineTrigger()
// βββ Idle βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ecs.defineState('idle')
.initial()
.onEnter(() => {
console.log('Entered Default State')
// Full-screen overlay layout
ecs.Ui.set(world, overlayFrame, {
type: 'overlay',
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
padding: '2%',
backgroundOpacity: 0.0,
opacity: 1.0,
justifyContent: 'flex-end',
alignItems: 'center',
gap: '0',
background: '#151414',
backgroundSize: 'contain',
borderRadius: 0,
})
// Top container holds only previewFrame
ecs.Ui.set(world, topFrame, {
type: 'overlay',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: '100%',
height: '80%',
backgroundOpacity: 0.0,
background: '#1F6ED6',
opacity: 1.0,
justifyContent: 'flex-end',
gap: '0',
padding: '0',
backgroundSize: 'contain',
borderRadius: 0,
})
// Preview frame: row layout for close button + image
ecs.Ui.set(world, previewFrame, {
type: 'overlay',
display: 'flex',
position: 'relative',
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'flex-start',
borderColor: '#FFFFF',
borderWidth: 2,
borderRadius: 12,
paddingBottom: '10',
backgroundOpacity: 0.0,
opacity: 1.0,
gap: '0',
background: '#141415',
backgroundSize: 'contain',
width: 'auto',
height: 'auto',
maxWidth: '90%',
maxHeight: '90%',
})
// Close button sits at top-right inside previewFrame
ecs.Ui.set(world, previewCloseButton, {
type: 'overlay',
position: 'absolute',
top: '36',
right: '36',
width: '36',
height: '36',
borderRadius: 18,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
background: '#AD50FF',
backgroundOpacity: 1,
opacity: 1.0,
flexDirection: 'row',
gap: '0',
padding: '0',
})
// Preview image auto-sizes to its intrinsic dimensions,
// with max 100% of frame and no shrink
ecs.Ui.set(world, previewImage, {
type: 'overlay',
maxWidth: '100%',
maxHeight: '100%',
overflow: 'hidden',
flexShrink: 0,
flexGrow: 1,
backgroundSize: 'contain',
backgroundOpacity: 0.00,
borderRadius: 0.00,
})
// Initial visibility
ecs.Hidden.set(world, topFrame)
ecs.Hidden.remove(world, captureButton)
ecs.Hidden.set(world, shareButton)
})
.onEvent(ecs.input.UI_CLICK, 'screenshot', {target: captureButton})
// βββ Screenshot ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ecs.defineState('screenshot')
.onEnter(() => {
// hide button
ecs.Hidden.set(world, captureButton)
// Hook into XR8 screenshot pipeline
XR8.addCameraPipelineModule(
XR8.canvasScreenshot().cameraPipelineModule()
)
// Capture screenshot
XR8.canvasScreenshot().takeScreenshot()
.then((base64: string) => {
data.imageData = base64
// Notify that screenshot is ready
toPreview.trigger()
})
.catch((err: any) => console.error('Screenshot error:', err))
})
// Move to preview once done
.onTrigger(toPreview, 'preview')
// βββ Preview βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ecs.defineState('preview')
.onEnter(() => {
ecs.Ui.mutate(world, previewImage, (cursor) => {
cursor.image = `data:image/png;base64,${data.imageData}`
})
world.time.setTimeout(() => {
// show imagepreview
ecs.Hidden.remove(world, topFrame)
// show closeButton
ecs.Hidden.remove(world, shareButton)
}, 40)
})
.onEvent(
ecs.input.UI_CLICK,
'idle',
{target: previewCloseButton}
)
.listen(shareButton, ecs.input.UI_CLICK, async () => {
console.log('share button cliked')
try {
// 1) Decode base64 to bytes
const b64 = data.imageData
const binary = atob(b64)
const len = binary.length
const bytes = new Uint8Array(len)
for (let i = 0; i < len; i++) {
bytes[i] = binary.charCodeAt(i)
}
// 2) Create a Blob (PNG)
const blob = new Blob([bytes], {type: 'image/png'})
// 3) Invoke native share
await shareImage(blob, 'screenshot.png')
// 4) Go back to idle (or close preview)
toIdle.trigger()
} catch (err) {
console.error('Share failed:', err)
}
})
},
})
// share.ts
// Make this file a module so our `declare global` takes effect:
export {}
// --- TYPE AUGMENTATION WORKAROUND ----------------------
declare global {
interface Navigator {
/** Web Share API L2: share File objects */
canShare?(data: ShareData): boolean;
}
interface ShareData {
/** Web Share API L2: an array of File objects to be shared */
files?: File[];
}
}
// --- UTILITY FUNCTION---------------------------------------
/**
* Shares an image blob using the Web Share API Level 2.
* @param blob The image Blob you want to share.
* @param fileName Optional filename (defaults to "image.jpg").
*/
export async function shareImage(
blob: Blob,
fileName = 'image.jpg'
): Promise<void> {
const file = new File([blob], fileName, {type: blob.type})
if (!navigator.canShare?.({files: [file]})) {
console.warn('Native file sharing is not supported on this browser.')
return
}
try {
await navigator.share({
files: [file],
title: 'Check out this image',
text: 'Sharing via Web Share API Level 2',
})
console.log('Share successful')
} catch (err) {
console.error('Share failed:', err)
}
}