Glitch: Streaming Video Implementation

GitHub Repos

💾 Ncoughlin: React-Streams-Client

💾 Ncoughlin: React-Streams-API

Intro

We will continue here working on our Twitch Clone Project called ”🏷️ Glitch“.

At this point we have full CRUD operations for stream creation, we have user authentication, and we have absolutely no video ability on our Twitch Clone. This is actually the least interesting part of this project, because the point is to get good at React, and the video streaming bit has so many fewer use cases that the general React knowledge that we have gained. In the interest of wrapping up a complete project however, let us forge ahead. Here is the basic structure of our application.

Glitch application overview

So far we have only been working with the Glitch Client, and the Glitch Database (Which is our JSON Server)

Now, in order to actually stream video to users we must set up an RTMP server. For a simple explanation of what an RTMP server is there is a good article here Stackpath.com/rtmp.

We are going to be using an RTMP server software called Node Media Server. In addition we will need to set up a video player in our Glitch Client, and for that we are going to use a piece of software called FLV.js.

The broadcasters will stream to the RTMP server. The RTMP server will make the stream available to requests, and FLV player integrated into the Glitch Client will request and format the video stream (similar to Axios for API requests) for display in our video HTML element.

Implementing FLV Video Player

We can start by following the installation instructions on the FLV Github page. And after we have installed the package they have a Getting Started quick guide, that currently looks like this.

<script src="flv.min.js"></script>
<video id="videoElement"></video>
<script>
    if (flvjs.isSupported()) {
        var videoElement = document.getElementById('videoElement');
        var flvPlayer = flvjs.createPlayer({
            type: 'flv',
            url: 'http://example.com/flv/video.flv'
        });
        flvPlayer.attachMediaElement(videoElement);
        flvPlayer.load();
        flvPlayer.play();
    }
</script>

And we need to take this and implement this into our React StreamShow component, the React way.

components/streams/StreamShow.js
import React from "react";
import flv from 'flv.js';import { connect } from "react-redux";
import { fetchStream } from "../../actions";

class StreamShow extends React.Component {
  constructor(props) {    super(props);    this.videoRef = React.createRef();  }
  componentDidMount() {
    this.props.fetchStream(this.props.match.params.id);
  }

  render() {
    if (!this.props.stream) {
      return <div>Loading...</div>;
    }

    const { title, description } = this.props.stream;

    return (
      <div>
        <video ref={this.videoRef} style={{ width: '100%' }} controls />        <h1>{title}</h1>
        <h5>{description}</h5>
      </div>
    );
  }
}

const mapStateToProps = (state, ownProps) => {
  return { stream: state.streams[ownProps.match.params.id] };
};

export default connect(mapStateToProps, { fetchStream })(StreamShow);

and we now have a blank video player available to us with reference videoRef.

blank video player

Note that the super(props) context syntax is deprecated now. I will be covering the new context system soon. This application is using React v16 so this is not a problem in this case.

It’s important to note that the video player did not come from the FLV package. The video player is a standard HTML element that is provided by the browser. FLV’s job is to download the video and convert it to a format that can be played inside of this HTML video player. In that sense FLV is very similar to Axios. It’s going to reach out and get some data, and present it to us in a way that can be consumed by our application.

Let’s try to configure this video player now, just as we need to configure Axios.

components/streams/StreamShow.js
import React from "react";
import flv from 'flv.js';
import { connect } from "react-redux";
import { fetchStream } from "../../actions";

class StreamShow extends React.Component {
  constructor(props) {
    super(props);

    this.videoRef = React.createRef();
  }

  componentDidMount() {
    // de-structure stream id variable
    const { id } = this.props.match.params

    // get stream data
    this.props.fetchStream(id);

    // create flv player    this.player = flv.createPlayer({      type: 'flv',      // url specified in RTMP setup      url: `http://localhost:2000/live/${id}.flv`    });    this.player.attachMediaElement(this.videoRef.current);      this.player.load();  }


  render() {
    if (!this.props.stream) {
      return <div>Loading...</div>;
    }

    const { title, description } = this.props.stream;

    return (
      <div>
        <video ref={this.videoRef} style={{ width: '100%' }} controls />
        <h1>{title}</h1>
        <h5>{description}</h5>
      </div>
    );
  }
}

const mapStateToProps = (state, ownProps) => {
  return { stream: state.streams[ownProps.match.params.id] };
};

export default connect(mapStateToProps, { fetchStream })(StreamShow);

Great let’s see if that works…

mediaElement is null error

And it’s a disaster. After some investigation the cause of the error is our little if statement there in the render function.

if (!this.props.stream) {
      return <div>Loading...</div>;
    }

The reference to the video element is failing because there is no video element when the component first mounts, just this little loading div. We need to find a way to prevent the creation of the flv player until after the stream has loaded… THEN fetch the video stream after the component has fetched the stream data that it needs.

If we look below we can see that we have broken the player building function out into a helper function, which will immediately exit if either of two conditions are true.

  1. The player has already been built
  2. There is no stream data yet
components/streams/StreamShow.js
import React from "react";
import flv from "flv.js";
import { connect } from "react-redux";
import { fetchStream } from "../../actions";

class StreamShow extends React.Component {
  constructor(props) {
    super(props);

    this.videoRef = React.createRef();
  }

  componentDidMount() {
    // de-structure stream id variable
    const { id } = this.props.match.params;

    // get stream data
    this.props.fetchStream(id);

    // attempt to build player
    this.buildPlayer();
  }

  componentDidUpdate() {
    // attempt to build player if not successful
    // on initial load
    this.buildPlayer();
  }

  // do not build player if player already exists
  // or stream has not been fetched
  buildPlayer() {
    if (this.player || !this.props.stream) {
      return;
    }

    // de-structure stream id variable
    const { id } = this.props.match.params;

    // create flv player
    this.player = flv.createPlayer({
      type: "flv",
      // url specified in RTMP setup
      url: `http://localhost:2000/live/${id}.flv`,
    });
    this.player.attachMediaElement(this.videoRef.current);
    this.player.load();
  }

  render() {
    if (!this.props.stream) {
      return <div>Loading...</div>;
    }

    const { title, description } = this.props.stream;

    return (
      <div>
        <video ref={this.videoRef} style={{ width: "100%" }} controls />
        <h1>{title}</h1>
        <h5>{description}</h5>
      </div>
    );
  }
}

const mapStateToProps = (state, ownProps) => {
  return { stream: state.streams[ownProps.match.params.id] };
};

export default connect(mapStateToProps, { fetchStream })(StreamShow);

The reason that we also called the buildPlayer function inside of componentDidUpdate is because if we load the page without going through the stream list first the player will not have been built, and we need to have a way to trigger the buildPlayer function again once the stream has been successfully fetched. And this does fix our error.

Streaming A Video

Let’s go ahead and get a real stream sent over to our RTMP server and get this app working for real.

Inside of OBS we create a stream with the settings that OBS asks for, namingly the url and our code, which we are just using the stream id for now. And voila.

it works

Unmounting Stream

One last thing we need to take care of is to destroy the player object when the user is not looking at the stream. If we don’t do that the stream won’t work again if we go back to the stream list and then back to the viewing page again.

components/streams/StreamShow.js
import React from "react";
import flv from "flv.js";
import { connect } from "react-redux";
import { fetchStream } from "../../actions";

class StreamShow extends React.Component {
  constructor(props) {
    super(props);

    this.videoRef = React.createRef();
  }

  componentDidMount() {
    // de-structure stream id variable
    const { id } = this.props.match.params;

    // get stream data
    this.props.fetchStream(id);

    // attempt to build player
    this.buildPlayer();
  }

  componentDidUpdate() {
    // attempt to build player if not successful
    // on initial load
    this.buildPlayer();
  }

  // clean up player if user leaves viewing page
  componentWillUnmount() {    this.player.destroy();  }
  // do not build player if player already exists
  // or stream has not been fetched
  buildPlayer() {
    if (this.player || !this.props.stream) {
      return;
    }

    // de-structure stream id variable
    const { id } = this.props.match.params;

    // create flv player
    this.player = flv.createPlayer({
      type: "flv",
      // url specified in RTMP setup
      url: `http://localhost:2000/live/${id}.flv`,
    });
    this.player.attachMediaElement(this.videoRef.current);
    this.player.load();
  }

  render() {
    if (!this.props.stream) {
      return <div>Loading...</div>;
    }

    const { title, description } = this.props.stream;

    return (
      <div>
        <video ref={this.videoRef} style={{ width: "100%" }} controls />
        <h1>{title}</h1>
        <h5>{description}</h5>
      </div>
    );
  }
}

const mapStateToProps = (state, ownProps) => {
  return { stream: state.streams[ownProps.match.params.id] };
};

export default connect(mapStateToProps, { fetchStream })(StreamShow);