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