Adding Video Recording/ Sharing to Niantic Studio Project

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)
  }
}