Image Target Video Gallery With Button Interaction

Hello, Fairly new to 8th wall. I want to create a video gallery overlaying an image target. The functionality I’m aiming for includes:

  1. When the image target is detected, an initial video plays automatically, overlaying the target.
  2. The user can interact with buttons displayed in the AR scene. Each button corresponds to a specific video.
  3. When a button is clicked:
  • The initial video stops and is replaced by the video corresponding to the clicked button.
  • The new video should then play, still overlaying the same image target.

The key challenge I’m facing is getting the initial video to stop and become invisible when the new video is triggered, while ensuring a seamless transition between videos.

I’ve tried approaches such as hiding the initial video using CSS (display: none) or using A-Frame attributes (visible), but I’m struggling to make it work consistently with xrextras-target-video-sound. Additionally, I attempted swapping between two separate image target entities but ran into difficulties managing visibility and playback states.

Does anyone have suggestions for managing video swapping while maintaining a smooth user experience? I’d appreciate insights, best practices, or example code!

Hi, Welcome to the forums!

Would you be able to post a video of the current behavior along with the code?

Hey George,

Thank you in advance for your help here!

Here is a link to a screen recording. In the recording, I alternate between clicking the two buttons. When I click the “Start Video Button”, the experience works as intended. However, when I click the “Video 1 Button”, the audio of the corresponding video starts playing, but the video itself is not visible. Additionally, the “Start Video” pauses and remains visible. My ultimate goal here is to have 6 buttons allowing the user to cycle seamlessly through 6 different videos.

Below is the code:

app.js

// Import components and primitives
import { myNamedImageTargetComponent, namedImageTargetPrimitive } from './my-named-target';

// Register component
AFRAME.registerComponent('my-xrextras-named-imagetarget', myNamedImageTargetComponent);
AFRAME.registerPrimitive('my-xrextras-named-image-target', namedImageTargetPrimitive);

// Component to handle video playback on button click
AFRAME.registerComponent('play-video', {
  schema: { videoId: { type: 'string' } }, // ID of the video to play

  init: function () {
    this.el.addEventListener('click', () => {
      const videoId = this.data.videoId; // Get the video ID from schema
      const videoPlayer = document.querySelector('#video-player'); // Get the video player element
      const videoElement = document.querySelector(videoId); // Get the actual video element by ID

      if (!videoElement) {
        console.error(`Video with ID ${videoId} not found.`);
        return;
      }

      // Pause and reset the currently playing video if any
      const currentVideoId = videoPlayer.getAttribute('video');
      if (currentVideoId) {
        const currentVideoElement = document.querySelector(currentVideoId);
        if (currentVideoElement) {
          currentVideoElement.pause();
          currentVideoElement.currentTime = 0; // Reset to the beginning
        }
      }

      // Update the video source in xrextras-target-video-sound component
      videoPlayer.setAttribute('src', videoElement.getAttribute('src'));

      // Trigger the new video to play directly
      videoElement.play();

      // Manually trigger xrextras-target-video-sound component to reload the video source
      videoPlayer.components['xrextras-target-video-sound'].play();
    });
  },
});

console.log('App.js loaded and initialized.');


body.html

<a-scene
  device-orientation
  xrextras-gesture-detector
  landing-page
  xrextras-loading="
    loadBackgroundColor: #63ACA8;
    cameraBackgroundColor: #63ACA8;
    loadImage: #logo-icon;
    loadAnimation: pulse"
  xrextras-runtime-error
  renderer="colorManagement:true"
  xrweb="disableWorldTracking: true">

  <!-- Assets -->
  <a-assets>
    <img id="start-thumb" src="assets/start-thumbnail.jpg">
    <img id="logo-icon" src="./assets/logo-icon.png">

    <!-- Video Assets for the Gallery -->
    <video id="start-video" crossorigin="anonymous" loop="false" src="assets/start-video.mp4"></video>
    <video id="video-1" crossorigin="anonymous" loop="false" src="assets/video-1.mp4"></video>
  </a-assets>

  <!-- Camera and Lights -->
  <a-camera position="0 0 0" raycaster="objects: .cantap" cursor="fuse: false; rayOrigin: mouse;"></a-camera>
  <a-light type="directional" intensity="0.5" position="1 1 1"></a-light>
  <a-light type="ambient" intensity="0.7"></a-light>

  <!-- Image Target with Video -->
  <my-xrextras-named-image-target name="start-target">
    <xrextras-target-video-sound 
      video="#start-video" 
      thumb="#start-thumb" 
      height="1" 
      width="1.1" 
      id="video-player">
    </xrextras-target-video-sound>

   <!-- Buttons as part of the tracked entity -->
    <a-entity position="0 -1 0" id="button-container" visible="true">
     
  <a-plane
    position="-0.5 0 0"
    width="0.5"
    height="0.2"
    color="#63ACA8"
    class="cantap"
    text="value: Start Video; color: #F3EBCE; align: center; width: 4;"
    play-video="videoId: #start-video">
  </a-plane>

  <a-plane
    position="0.5 0 0"
    width="0.5"
    height="0.2"
    color="#63ACA8"
    class="cantap"
    text="value: Video 1; color: #F3EBCE; align: center; width: 4;"
    play-video="videoId: #video-1">
  </a-plane>

    </a-entity>
  </my-xrextras-named-image-target>

  </my-xrextras-named-image-target>

</a-scene>

my-named-target.js

const myNamedImageTargetComponent = {
  schema: {
    name: { type: 'string' },
  },
  init() {
    const { object3D } = this.el;
    const { name } = this.data;
    const geometry = {};

    const onready = () => {
      this.el.sceneEl.removeEventListener('realityready', onready);
      object3D.visible = false;
    };

    this.el.sceneEl.addEventListener('realityready', onready);

    const checkGeometry = (newGeometry) => {
      let needsUpdate = false;
      const fields = [
        'type',
        'height',
        'radiusTop',
        'radiusBottom',
        'arcLengthRadians',
        'arcStartRadians',
        'scaledWidth',
        'scaledHeight',
      ];

      fields.forEach((f) => {
        if (geometry[f] !== newGeometry[f]) {
          geometry[f] = newGeometry[f];
          needsUpdate = true;
        }
      });

      if (needsUpdate) {
        this.el.emit('xrextrasimagegeometry', geometry, false);
      }
    };

    const imageScanning = ({ detail }) => {
      detail.imageTargets.forEach((t) => {
        if (name !== t.name) return;
        checkGeometry({ type: t.type, ...t.geometry });
      });
    };

    const updateImage = ({ detail }) => {
      if (name !== detail.name) return;
      object3D.position.copy(detail.position);
      object3D.quaternion.copy(detail.rotation);
      object3D.scale.set(detail.scale, detail.scale, detail.scale);
      object3D.visible = true;
    };

    const showImage = ({ detail }) => {
      if (name !== detail.name) return;
      checkGeometry(detail);
      updateImage({ detail });
      this.el.emit('xrextrasfound', {}, false);
    };

    const hideImage = ({ detail }) => {
      if (name !== detail.name) return;
      this.el.emit('xrextraslost', {}, false);
      object3D.visible = false;
    };

    this.el.sceneEl.addEventListener('xrimagescanning', imageScanning);
    this.el.sceneEl.addEventListener('xrimagefound', showImage);
    this.el.sceneEl.addEventListener('xrimageupdated', updateImage);
    this.el.sceneEl.addEventListener('xrimagelost', hideImage);
  },
};

const namedImageTargetPrimitive = {
  defaultComponents: {
    'my-xrextras-named-imagetarget': {},
  },
  mappings: {
    name: 'my-xrextras-named-imagetarget.name',
  },
};

export { myNamedImageTargetComponent, namedImageTargetPrimitive };

Can you share your project with support@8thwall.com?

1 Like

Just shared!

Thank you for your time.

Hey @GeorgeButler, Just wanted to see if you were able to get access to the project?

Thank you!

Hey Irving!

Thanks for the patience! Our office just returned from a Holiday. Can you share it with the support workspace? It’s the same process just without the email address part.

Thanks @GeorgeButler! Just sent an invite to share with support.

Got it, I’ll take a look.

Hey @GeorgeButler, Just checking to see if you might have had a chance to take a look here. Thank you!