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