Video/Image capture

What are the options now for adding video/Image capture?

This is something we’re actively working on making into a sample project.

Do you have an ETA on that? I’m in the middle of doing early dev work for a live project trying to get various functionality working and I could really do with a solution to this

Here’s a very quick example I made.

import * as ecs from '@8thwall/ecs'

ecs.registerComponent({
  name: 'Capture Screenshot',
  schema: {
    screenshotButton: ecs.eid,
    previewImage: ecs.eid,
  },
  stateMachine: ({world, eid, schemaAttribute, dataAttribute}) => {
    const toLoaded = ecs.defineTrigger()

    ecs.defineState('default')
      .initial()
      .onEnter(() => {
        const {previewImage} = schemaAttribute.get(eid)

        ecs.Hidden.set(world, previewImage)
      })
      .onTick(() => {
        // @ts-ignore
        if (window.XR8) {
          world.time.setTimeout(() => {
            toLoaded.trigger()
          }, 500)
        }
      })
      .onTrigger(toLoaded, 'loaded')

    ecs.defineState('loaded')
      .onEnter(() => {
        const {XR8} = window as any

        XR8.addCameraPipelineModule(XR8.CanvasScreenshot.pipelineModule())
      })
      .listen(schemaAttribute.get(eid).screenshotButton, ecs.input.UI_CLICK, (e) => {
        const {XR8} = window as any
        const {previewImage} = schemaAttribute.get(eid)

        XR8.CanvasScreenshot
          .takeScreenshot()
          .then((data) => {
            ecs.Ui.mutate(world, previewImage, (c) => {
              c.image = `data:image/png;base64,${data}`
            })

            ecs.Hidden.remove(world, previewImage)
          }, (error) => {
            console.log(error)
            // Handle screenshot error.
          })
      })
  },
})

Here’s a video of it in use and the component setup.

this is how I added native share functionality if anyone is interested:


import * as ecs from '@8thwall/ecs'  

// ─── HELPERS ─────────────────────────────────────────────────────────────

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[];
  }
}

async function shareImage(
  blob: Blob,
  fileName = 'ReunionTower.jpg',
  title = '',
  text = ''
): 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,
      text,
    })
    console.log('Share successful')
  } catch (err) {
    console.error('Share failed:', err)
  }
}

// ─── COMPONENT ────────────────────────────────────────────────────────────

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,
    shareTitle: ecs.string,
    shareText: ecs.string,
  },
  schemaDefaults: {
    shareTitle: 'Default Title',
    shareText: 'Look what I capture?! Custom share copy blabla..',
  },
  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,
      shareTitle,
      shareText,
    } = schemaAttribute.get(eid)
    const data = dataAttribute.cursor(eid)
    const toPreview = ecs.defineTrigger()
    const toIdle = ecs.defineTrigger()

    // One blob at a time, per-component instance:
    let capturedBlob: Blob | null = null

    // ─── Idle ───────────────────────────────────────────────────────────
    ecs.defineState('idle')
      .initial()
      .onEnter(() => {

        // Initial visibility
        ecs.Hidden.set(world, topFrame)
        ecs.Hidden.remove(world, captureButton)
        ecs.Hidden.set(world, shareButton)
        // clear old blob
        capturedBlob = null
      })
      .onEvent(ecs.input.UI_CLICK, 'screenshot', {target: captureButton})

    // ─── Screenshot ──────────────────────────────────────────────────────
    ecs.defineState('screenshot')
      .onEnter(() => {
        // hide button
        ecs.Hidden.set(world, captureButton)
        const {XR8} = window as any
        // Hook into XR8 screenshot pipeline
        XR8.addCameraPipelineModule(
          XR8.canvasScreenshot().cameraPipelineModule()
        )
        // Capture screenshot
        XR8.canvasScreenshot().takeScreenshot()
          .then(async (base64: string) => {
            data.imageData = base64

            // convert immediately via fetch(dataURL)
            const dataUrl = `data:image/png;base64,${base64}`
            const resp = await fetch(dataUrl)
            capturedBlob = await resp.blob()

            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 clicked')
        try {
          if (!capturedBlob) {
            console.warn('Still processing screenshot…')
            return
          }
          await shareImage(capturedBlob, 'screenshot.png', shareTitle, shareText)
        } catch (err) {
          console.error('Share failed:', err)
        }
      })
  },
})
1 Like

Thanks both for your help

I had a working prototype with using this code which I’ve not checked on in a few weeks. Come back today and it’s throwing errors across multiple things. @GeorgeButler has there been an update to the editor?

Not that I’m aware of. Can you share the errors?

I emailed support about this - it seems like your UI is broken - can’t nest more that two elements, positioning not working etc.

So. This is driving me slightly mad. I’m new to 8th wall. Is it actually impossible to add the functionality for the user to take a photo or video of what they’re seeing on screen? p.s. I’m new, so posting a bit of code doesn’t help me - I don’t even know where it goes? But then, surely if it was easy, it would exist?

Trying to follow this but I’m baffled…

Yeah this is a core component tbh and should be provided in the platform library to add to projects and allows visual customisation

1 Like

Really could use this right about now

tried several of these examples and none worked.

This script works for me (typescript):

import * as ecs from '@8thwall/ecs'  

// ─── HELPERS ─────────────────────────────────────────────────────────────

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[];
  }
}

async function shareImage(
  blob: Blob,
  fileName = 'ReunionTower.jpg',
  title = '',
  text = ''
): 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,
      text,
    })
    console.log('Share successful')
  } catch (err) {
    console.error('Share failed:', err)
  }
}

// ─── COMPONENT ────────────────────────────────────────────────────────────

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,
    shareTitle: ecs.string,
    shareText: ecs.string,
  },
  schemaDefaults: {
    shareTitle: 'Default Title',
    shareText: 'Look what I capture?! Custom share copy blabla..',
  },
  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,
      shareTitle,
      shareText,
    } = schemaAttribute.get(eid)
    const data = dataAttribute.cursor(eid)
    const toPreview = ecs.defineTrigger()
    const toIdle = ecs.defineTrigger()

    // One blob at a time, per-component instance:
    let capturedBlob: Blob | null = null

    // ─── Idle ───────────────────────────────────────────────────────────
    ecs.defineState('idle')
      .initial()
      .onEnter(() => {

        // Initial visibility
                  ecs.Ui.set(world, shareButton, {
  left:'0'
})
        ecs.Hidden.set(world, previewFrame)
        ecs.Hidden.remove(world, captureButton)
        ecs.Hidden.set(world, shareButton)
        // clear old blob
        capturedBlob = null
      })
      .onEvent(ecs.input.UI_CLICK, 'screenshot', {target: captureButton})

    // ─── Screenshot ──────────────────────────────────────────────────────
    ecs.defineState('screenshot')
      .onEnter(() => {
        // hide button
        ecs.Hidden.set(world, captureButton)
        const {XR8} = window as any
        // Hook into XR8 screenshot pipeline
         XR8.addCameraPipelineModule(XR8.CanvasScreenshot.pipelineModule())
        // Capture screenshot
        XR8.CanvasScreenshot.takeScreenshot()
          .then(async (base64: string) => {
            data.imageData = base64

            // convert immediately via fetch(dataURL)
            const dataUrl = `data:image/png;base64,${base64}`
            const resp = await fetch(dataUrl)
            capturedBlob = await resp.blob()
            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(() => {

          ecs.Ui.set(world, shareButton, {
  left:'36%'
})



          // show imagepreview
          ecs.Hidden.remove(world, previewFrame)
          // 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 clicked')
        try {
          if (!capturedBlob) {
            console.warn('Still processing screenshot…')
            return
          }
          await shareImage(capturedBlob, 'screenshot.png', shareTitle, shareText)
        } catch (err) {
          console.error('Share failed:', err)
        }
      })
  },
})

Didn’t work for me but my project started running jankily.

Can you give more of an idea of what didn’t work? I’m using this script in a build I’m current working on and it’s working fine for me.

can I see a screenshot of your project? Mine is using a target to trigger animation and sound and I need to integrate a button because it’s for a museum and people like taking photos even though that’s not really what it’s for. Looking at this script, i see that there is. a UI, share button, so I put in a button to use with this script. I was wondering if I need to attach it like in Snap but it doesn’t allow. Am I missing something?


So I’ve attached the script to my capture button. Select the button then in the info panel on the right go to new component->custom component and add the screenshot-capture script. Once it loads it give you the option to assign UI element etc. Preview image is a blank image to show the snapshot, then there’s the other UI buttons etc.

Ok, so I did this and…still nothing is happening.