Skip to main content
Documentation

Recording State

Record and replay Five SDK state changes in React class components and HOCs.

Recap of the previous chapter: Changing the Viewpoint

You learned what State is, and how to read and modify it. Using State, you built an auto look-around feature.

In this chapter you will learn

  1. How to record user actions through State.
  2. How to replay user actions through State.

Getting Ready

As in the previous chapter, create a new directory (src/3.recording-state) along with its corresponding html file and jsx or tsx files.

You can start by copying the contents of the previous chapter into the jsx or tsx files.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="data:;base64,iVBORw0KGgo=" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Recording State</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      html,
      body #app {
        width: 100%;
        height: 100%;
        overflow: hidden;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="./index"></script>
  </body>
</html>
import React, { Component } from "react";
import { parseWork } from "@realsee/five";

/**
 * React HOC that fetches the work
 * @param url the URL of work.json
 */
function withFetchWork(url) {
  return function (Compnent) {
    return class extends Component {
      state = { work: null };
      componentDidMount() {
        fetch(url)
          .then((res) => res.json())
          .then((json) => {
            this.setState({ work: parseWork(json) });
          });
      }
      render() {
        if (this.state.work === null) return null;
        return <Compnent work={this.state.work} {...this.props} />;
      }
    };
  };
}

export { withFetchWork };
import React, { Component } from "react";

/**
 * React HOC: gets the current window dimensions
 */
function withWindowDimensions() {
  return function (Compnent) {
    return class extends Component {
      state = this.getWindowDimensions();
      resizeListener = () => {
        this.setState(this.getWindowDimensions());
      };
      getWindowDimensions() {
        return { width: window.innerWidth, height: window.innerHeight };
      }
      componentDidMount() {
        window.addEventListener("resize", this.resizeListener, false);
      }
      componentWillUnmount() {
        window.removeEventListener("resize", this.resizeListener, false);
      }
      render() {
        const dimensions = {
          width: this.state.width,
          height: this.state.height,
        };
        return <Compnent windowDimensions={dimensions} {...this.props} />;
      }
    };
  };
}

export { withWindowDimensions };
import React, { Component } from "react";
import { Five } from "@realsee/five";
import { withFive, createFiveFeature } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import BottomNavigation from "@mui/material/BottomNavigation";
import BottomNavigationAction from "@mui/material/BottomNavigationAction";
import Paper from "@mui/material/Paper";
import DirectionsWalkIcon from "@mui/icons-material/DirectionsWalk";
import ViewInArIcon from "@mui/icons-material/ViewInAr";

const FEATURES = createFiveFeature("currentState", "setState");

/**
 * React Component: mode control
 */
const ModeController = compose(withFive(FEATURES))(
  class extends Component {
    render() {
      return (
        <Paper sx={{ position: "fixed", bottom: 0, left: 0, right: 0 }}>
          <BottomNavigation
            showLabels
            value={this.props.$five.currentState.mode}
            onChange={(_, newValue) => {
              this.props.$five.setState({ mode: newValue });
            }}
          >
            <BottomNavigationAction
              label="Panorama Walkthrough"
              icon={<DirectionsWalkIcon />}
              value={Five.Mode.Panorama}
            />
            <BottomNavigationAction
              label="Space Overview"
              icon={<ViewInArIcon />}
              value={Five.Mode.Floorplan}
            />
          </BottomNavigation>
        </Paper>
      );
    }
  }
);

export { ModeController };
import React, { Component } from "react";
import { withFive, createFiveFeature } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import IconButton from "@mui/material/IconButton";
import Paper from "@mui/material/Paper";
import FlipCameraAndroidIcon from "@mui/icons-material/FlipCameraAndroid";
import PauseIcon from "@mui/icons-material/Pause";

const FEATURES = createFiveFeature("currentState", "setState");

/**
 * ReactComponent: auto look-around button
 */
const LookAroundController = compose(withFive(FEATURES))(
  class extends Component {
    timer;
    state = { active: false };

    toggleActive(active) {
      window.clearInterval(this.timer);
      this.setState({ active });
      if (active === true) {
        this.timer = window.setInterval(() => {
          this.props.$five.setState({
            longitude: this.props.$five.currentState.longitude + Math.PI / 360,
          });
        }, 16);
      } else {
        delete this.timer;
      }
    }
    render() {
      return (
        <Paper sx={{ position: "fixed", top: 10, right: 10 }}>
          {this.state.active ? (
            <IconButton onClick={() => this.toggleActive(false)}>
              <PauseIcon />
            </IconButton>
          ) : (
            <IconButton onClick={() => this.toggleActive(true)}>
              <FlipCameraAndroidIcon />
            </IconButton>
          )}
        </Paper>
      );
    }
  }
);

export { LookAroundController };
import React, { Component } from "react";
import { compose } from "@wordpress/compose";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { withFetchWork } from "./withFetchWork";
import { withWindowDimensions } from "./withWindowDimensions";
import { ModeController } from "./ModeController";
import { LookAroundController } from "./LookAroundController";

/** Data URL of work.json */
const workURL =
  "https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";

const FiveProvider = createFiveProvider();

const App = compose(
  withFetchWork(workURL),
  withWindowDimensions()
)(
  class extends Component {
    render() {
      const { work, windowDimensions } = this.props;
      return (
        <FiveProvider initialWork={work}>
          <FiveCanvas
            width={windowDimensions.width}
            height={windowDimensions.height}
          />
          <ModeController />;
          <LookAroundController />;
        </FiveProvider>
      );
    }
  }
);

export { App };
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";

ReactDOM.render(<App />, document.querySelector("#app"));

export {};

Start the dev server with npm run dev, then navigate to the current page at "http://localhost:3000/src/3.recording-state/index.html".

Info

Please check your console: the port number may change depending on your configuration and which ports are currently in use, so always trust what the console prints. If you use a different build tool, start the server according to that tool's requirements.

Record / Replay

In this chapter we will continue to build an interesting application using State.

We will build an application that records the State changes that happen on the page and can replay those actions.

Writing the Recorder Class

First, we need to write a Recorder class to support recording and replaying. The Recorder class is not part of Five SDK; it is written purely to achieve the effect of this chapter.

  1. Implement the startRecording / endRecording methods to start and stop recording.
  2. Implement the record(state: State) method to record content. It records everything between startRecording and endRecording.
  3. Implement the play(callback) method for replaying. After calling play, it walks through the recorded record content and invokes the callback method in sequence to replay the State.
/**
 * Recorder class
 */
class Recorder {
  constructor() {
    this.startTime = 0;
    this.records = null;
  }
  /**
   * Whether anything has been recorded
   */
  hasRecords() {
    return this.records !== null;
  }
  /**
   * Record a keyframe
   * @param state the five state
   * @returns
   */
  record(state) {
    if (this.records === null) return;
    this.records.push({
      state: Object.assign({}, state),
      time: Date.now() - this.startTime,
    });
  }
  /**
   * Start recording
   */
  startRecording() {
    this.startTime = Date.now();
    this.records = [];
  }
  /**
   * Stop recording
   */
  endRecording() {
    this.startTime = 0;
  }
  /**
   * Replay the recording
   * @param callback keyframe callback
   * @returns whether there is currently a recording
   */
  play(callback) {
    if (this.records === null || this.records.length === 0) return false;
    const records = this.records.slice();
    const keyframe = (keyIndex) => {
      const current = records[keyIndex];
      const next = records[keyIndex + 1];
      callback(current.state, next === undefined);
      if (next) {
        const delay = next.time - current.time;
        setTimeout(() => keyframe(keyIndex + 1), delay);
      }
    };
    keyframe(0);
    return true;
  }
}

export { Recorder };

Writing the Recorder Component

Wrap the Recorder class into a React component.

  1. Add a RecorderController file to hold the component.
  2. The component has two React state values, recording and playing, representing the recording and replaying states respectively.
  3. `useFiveEventCallback` lets you hook into Five SDK's built-in event callbacks.

Here we listen to the stateChange event, which fires whenever the state changes, and then record the State by calling recorder.record(state).

For more about events, see the Five SDK event list.

  1. When the replay button is pressed, calling recorder.play(callback) invokes the callback method for each previously recorded state in turn, replaying the records.
  2. Each record is applied by calling Five SDK's setState method, which updates the view.
import React, { Component } from "react";
import { withFive, createFiveFeature } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import Paper from "@mui/material/Paper";
import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
import StopIcon from "@mui/icons-material/Stop";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import { Recorder } from "./recorder";

const FEATURES = createFiveFeature("state", "setState", "on", "off");

/**
 * ReactComponent: state recording
 */
const RecorderController = compose(withFive(FEATURES))(
  class extends Component {
    recorder = new Recorder();
    state = { recording: false, playing: false };

    startRecording = () => {
      this.recorder.startRecording();
      this.setState({ recording: true });
    };

    endRecording = () => {
      this.recorder.endRecording();
      this.setState({ recording: false });
    };

    play = () => {
      const hasRecord = this.recorder.play((state, isFinal) => {
        this.props.$five.setState(state);
        this.setState({ playing: !isFinal });
      });
      this.setState({ playing: hasRecord });
    };

    record = (state) => {
      this.recorder.record(state);
    };

    componentDidMount() {
      this.props.$five.on("stateChange", this.record);
    }

    componentWillUnmount() {
      this.props.$five.off("stateChange", this.record);
    }

    render() {
      if (this.state.recording) {
        return (
          <Paper sx={{ position: "fixed", top: 10, left: 10 }}>
            <IconButton onClick={this.endRecording}>
              <StopIcon />
            </IconButton>
            <Button disabled>Recording</Button>
          </Paper>
        );
      }
      if (this.state.playing) {
        return (
          <Paper sx={{ position: "fixed", top: 10, left: 10 }}>
            <Button disabled>Replaying</Button>
          </Paper>
        );
      }
      return (
        <Paper sx={{ position: "fixed", top: 10, left: 10 }}>
          <IconButton onClick={this.startRecording}>
            <FiberManualRecordIcon />
          </IconButton>
          <IconButton onClick={this.play}>
            <PlayArrowIcon />
          </IconButton>
        </Paper>
      );
    }
  }
);

export { RecorderController };

Using the State Recording Component

Insert it into the FiveProvider in the App file.

import React, { Component } from "react";
import { compose } from "@wordpress/compose";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { withFetchWork } from "./withFetchWork";
import { withWindowDimensions } from "./withWindowDimensions";
import { ModeController } from "./ModeController";
import { LookAroundController } from "./LookAroundController";
// highlight-start
import { RecorderController } from "./RecorderController";
// highlight-end

/** Data URL of work.json */
const workURL =
  "https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";

const FiveProvider = createFiveProvider();

const App = compose(
  withFetchWork(workURL),
  withWindowDimensions()
)(
  class extends Component {
    render() {
      const { work, windowDimensions } = this.props;
      return (
        <FiveProvider initialWork={work}>
          <FiveCanvas
            width={windowDimensions.width}
            height={windowDimensions.height}
          />
          <ModeController />;
          <LookAroundController />; // highlight-start
          <RecorderController />; // highlight-end
        </FiveProvider>
      );
    }
  }
);

export { App };

Go back to your browser and check: you will see a record button and a play button appear in the top-left corner of the page. Give them a try and see whether they behave as expected.

Impressive, you can already write fairly sophisticated programs now 🥳.

What You Will Learn in the Next Chapter

In the next chapter we will start working with models in 3D space

  • Understand the Five SDK's gesture interaction system.
  • Get the 3D position of a point.