DoneJS StealJS jQuery++ FuncUnit DocumentJS
6.0.1
5.33.2 4.3.0 3.14.1 2.3.35
  • About
  • Guides
  • API Docs
  • Community
  • Contributing
  • Bitovi
    • Bitovi.com
    • Blog
    • Design
    • Development
    • Training
    • Open Source
    • About
    • Contact Us
  • About
  • Guides
    • getting started
      • CRUD Guide
      • Setting Up CanJS
      • Technology Overview
    • topics
      • HTML
      • Routing
      • Service Layer
      • Debugging
      • Forms
      • Testing
      • Logic
      • Server-Side Rendering
    • app guides
      • Chat Guide
      • TodoMVC Guide
      • TodoMVC with StealJS
    • beginner recipes
      • Canvas Clock
      • Credit Card
      • File Navigator
      • Signup and Login
      • Video Player
    • intermediate recipes
      • CTA Bus Map
      • Multiple Modals
      • Text Editor
      • Tinder Carousel
    • advanced recipes
      • Credit Card
      • File Navigator
      • Playlist Editor
      • Search, List, Details
    • upgrade
      • Migrating to CanJS 3
      • Migrating to CanJS 4
      • Migrating to CanJS 5
      • Migrating to CanJS 6
      • Using Codemods
    • other
      • Reading the API Docs
  • API Docs
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

Video Player

  • Edit on GitHub

This beginner guide walks you through building custom video controls around a video element.

In this guide, you will learn how to create a custom video player using the <video> element and CanJS. The custom video player will:

  • Have custom play and pause buttons.
  • Show the current time and duration of the video.
  • Have a <input type="range"> slider that can adjust the position of the video.

The final player looks like:

See the Pen CanJS 6.0 Video Player - Final by Bitovi (@bitovi) on CodePen.

The following sections are broken down into the following parts:

  • The problem — A description of what the section is trying to accomplish.
  • What you need to know — Browser or CanJS APIs that are useful for solving the problem.
  • The solution — The solution to the problem.

Setup

START THIS TUTORIAL BY Forking THE FOLLOWING CodePen:

Click the Edit in CodePen button. The CodePen will open in a new window. Click the Fork button.

See the Pen CanJS 6.0 Video Player - Start by Bitovi (@bitovi) on CodePen.

This CodePen:

  • Creates a <video> element that loads a video. Right click and select “Show controls” to see the video’s controls.
  • Loads CanJS’s custom element library: StacheElement.

The problem

In this section, we will:

  • Create a custom <video-player> element that takes a src attribute and creates a <video> element within itself. We should be able to create the video like:

    <video-player src:raw="http://bit.ly/can-tom-n-jerry">
    </video-player>
    
  • The embedded <video> element should have the native controls enabled.

When complete, the result will look exactly the same as the player when you started. The only difference is that we will be using a custom <video-player> element in the HTML tab instead of the native <video> element.

What you need to know

To set up a basic CanJS application (or widget), you define a custom element in JavaScript and use the custom element in your page’s HTML.

To define a custom element, extend StacheElement and register a tag that matches the name of your custom element. For example:

class VideoPlayer extends StacheElement {
}
customElements.define("video-player", VideoPlayer);

Then you can use this tag in your HTML page:

<video-player></video-player>

But this doesn’t do anything ... yet. Components add their own HTML through their view property:

class VideoPlayer extends StacheElement {
  static view = `<h2>I am a player!</h2>`;
}
customElements.define("video-player", VideoPlayer);

A component’s view is rendered with its props. For example, we can make a <video> display "http://bit.ly/can-tom-n-jerry" by defining a src property and using it in the view like:

class VideoPlayer extends StacheElement {
  static view = `
    <video>
      <source src="{{ this.src }}">
    </video>
  `;
  static props = {
    src: {
      type: String,
      default: "http://bit.ly/can-tom-n-jerry"
    }
  };
}
customElements.define("video-player", VideoPlayer);

But we want the <video-player> to take a src attribute value itself and use that for the <source>’s src. For example, if we wanted the video to play "http://dl3.webmfiles.org/big-buck-bunny_trailer.webm" instead of "http://bit.ly/can-tom-n-jerry", we would:

  1. Update <video-player> to pass "http://dl3.webmfiles.org/big-buck-bunny_trailer.webm" with key:raw:
<video-player src:raw="http://dl3.webmfiles.org/big-buck-bunny_trailer.webm"/>
  1. Update the VideoPlayer element to define a src property like:
class VideoPlayer extends StacheElement {
  static view = `
    <video>
      <source src="{{ this.src }}">
    </video>
  `;
  static props = {
    src: String
  };
 }
 customElements.define("video-player", VideoPlayer);

Finally, to have a <video> element show the native controls, add a controls attribute like:

<video controls>

The solution

Update the JS tab to:

import { StacheElement } from "//unpkg.com/can@6/core.mjs";

class VideoPlayer extends StacheElement {
    static view = `
        <video controls>
            <source src="{{ this.src }}">
        </video>
    `;

    static props = {
        src: String
    };
}

customElements.define("video-player", VideoPlayer);

Update the HTML to:

<video-player src:raw="http://bit.ly/can-tom-n-jerry"></video-player>

Make play / pause button change as video is played and paused

The problem

When the video is played or paused using the native controls, we want to change the content of a <button> to say “Play” or “Pause”.

When the video is played, the button should say “Pause”. When the video is paused, the button should say “Play”.

We want the button to be within a <div> after the video element like:

</video>
<div>
  <button>Play</button>
</div>

What you need to know

  • To change the HTML content of the page, use if and {{else}} like:

    <button>{{# if(playing) }} Pause {{ else }} Play {{/ if }}</button>
    
  • The view responds to values in the props object. To create a boolean value, add it to the props object like:

    class VideoPlayer extends StacheElement {
      static props = {
        // ...
        playing: Boolean
      };
    }
    
  • Methods can be used to change values in props. The following might create methods that change the playing value:

    class VideoPlayer extends StacheElement {
      // ...
      play() {
        this.playing = true;
      }
      pause() {
        this.playing = false;
      }
    }
    
  • You can listen to events on the DOM with on:event. For example, the following might listen to a click on a <div> and call doSomething():

    <div on:click="doSomething()">
    

    <video> elements have a variety of useful events, including play and pause events that are emitted when the video is played or paused.

The solution

Update the JavaScript tab to:

import { StacheElement } from "//unpkg.com/can@6/core.mjs";

class VideoPlayer extends StacheElement {
  static view = `
        <video
            controls
            on:play="this.play()"
            on:pause="this.pause()"
        >
            <source src="{{ this.src }}">
        </video>
        <div>
            <button>
                {{# if(this.playing) }} Pause {{ else }} Play {{/ if }}
            </button>
        </div>
    `;

  static props = {
    src: String,
    playing: Boolean
  };

  play() {
    this.playing = true;
  }

  pause() {
    this.playing = false;
  }
}

customElements.define("video-player", VideoPlayer);

Make clicking the play/pause button play or pause the video

The problem

When the play/pause <button> we created in the previous section is clicked, we want to either play or pause the video.

What you need to know

The <video> player has state, such as if the video is playing. When the play/pause button is clicked, we want to update the state of the element props and have the element props update the state of the video player as a side effect.

What this means is that instead of something like:

togglePlay() {
  if (videoElement.paused) {
    videoElement.play()
  } else {
    videoElement.pause()
  }
}

We update the state like:

togglePlay() {
  this.playing = !this.playing;
}

And listen to when playing changes and update the video element like:

element.listenTo("playing", function({ value }) {
  if (value) {
    videoElement.play()
  } else {
    videoElement.pause()
  }
})

This means that you need to:

  1. Listen to when the <button> is clicked and call a method that updates the playing state.
  2. Listen to when the playing state changes and update the state of the <video> element.

You already know everything you need to know for step #1. (Have the button call a togglePlay method with on:click="togglePlay()" and make the togglePlay() method toggle the state of the playing property.)

For step #2, you need to use the connected lifecycle hook. This hook is a good place to do side-effects. Its use looks like this:

class MyComponent extends StacheElement {
  static view = `...`;
  static props = { /* ... */ };
  connected() {
    // `this` points to the element
    // perform mischief
  }
}

The connected hook gets called once the component’s element is in the page. You can use listenTo to listen to changes in the element's properties and perform side-effects. The following listens to when playing changes:

class VideoPlayer extends StacheElement {
  static view = `...`;
  static props = { /* ... */ };
  connected() {
    this.listenTo("playing", ({ value }) => {
    });
  }
}

Use querySelector to get the <video> element from the <video-player> like:

element.querySelector("video")

<video> elements have a .play() and .pause() methods that can start and stop a video.

The solution

Update the JavaScript tab to:

import { StacheElement } from "//unpkg.com/can@5/core.mjs";

class VideoPlayer extends StacheElement {
    static view = `
        <video 
            controls
            on:play="this.play()"
            on:pause="this.pause()"
        >
            <source src="{{ this.src }}">
        </video>
        <div>
            <button on:click="this.togglePlay()">
                {{# if(this.playing) }} Pause {{ else }} Play {{/ if }}
            </button>
        </div>
    `;

    static props = {
        src: String,
        playing: Boolean
    };

    connected() {
        this.listenTo("playing", function({ value }) {
            if (value) {
                this.querySelector("video").play();
            } else {
                this.querySelector("video").pause();
            }
        });
    }

    play() {
        this.playing = true;
    }

    pause() {
        this.playing = false;
    }

    togglePlay() {
        this.playing = !this.playing;
    }
}

customElements.define("video-player", VideoPlayer);

Show current time and duration

The problem

Show the current time and duration of the video element. The time and duration should be formatted like: mm:SS. They should be presented within two spans like:

</button>
<span>1:22</span>
<span>2:45</span>

What you need to know

  1. Methods can be used to format values in can-stache. For example, you can uppercase values like this:

    <span>{{ upper(value) }}</span>
    

    With a method like:

class MyComponent extends StacheElement {
  static view = `...`;
  static props = { /* ... */ };
  upper(value) {
    return value.toString().toUpperCase();
  }
 }

The following can be used to format time:

formatTime(time) {
  if (time === null || time === undefined) {
    return "--";
  }
  const minutes = Math.floor(time / 60);
  let seconds = Math.floor(time - minutes * 60);
  if (seconds < 10) {
    seconds = "0" + seconds;
  }
  return minutes + ":" + seconds;
}
  1. Time is given as a number. Use the following to create a number property on the element:
class VideoPlayer {
  static view = `...`;
  static props = {
    duration: Number,
    currentTime: Number
  };
}
  1. <video> elements emit a loadmetadata event when they know how long the video is. They also emit a timeupdate event when the video’s current play position changes.

    • videoElement.duration reads the duration of a video.
    • videoElement.currentTime reads the current play position of a video.
  2. You can get the element in an stache on:event binding with scope.element like:

    <video on:timeupdate="updateTimes(scope.element)"></video>
    

The solution

Update the JavaScript tab to:

import { StacheElement } from "//unpkg.com/can@6/core.mjs";

class VideoPlayer extends StacheElement {
    static view = `
        <video 
            controls
            on:play="this.play()"
            on:pause="this.pause()"
            on:timeupdate="this.updateTimes(scope.element)"
            on:loadedmetadata="this.updateTimes(scope.element)"
        >
            <source src="{{ this.src }}">
        </video>
        <div>
            <button on:click="this.togglePlay()">
                {{# if(this.playing) }} Pause {{ else }} Play {{/ if }}
            </button>
            <span>{{ this.formatTime(this.currentTime) }}</span> /
            <span>{{ this.formatTime(this.duration) }} </span>
        </div>
    `;

    static props = {
        src: String,
        playing: Boolean,
        duration: Number,
        currentTime: Number
    };

    connected() {
        this.listenTo("playing", function({ value }) {
            if (value) {
                this.querySelector("video").play();
            } else {
                this.querySelector("video").pause();
            }
        });
    }

    updateTimes(videoElement) {
        this.currentTime = videoElement.currentTime || 0;
        this.duration = videoElement.duration;
    }

    formatTime(time) {
        if (time === null || time === undefined) {
            return "--";
        }
        const minutes = Math.floor(time / 60);
        let seconds = Math.floor(time - minutes * 60);
        if (seconds < 10) {
            seconds = "0" + seconds;
        }
        return minutes + ":" + seconds;
    }

    play() {
        this.playing = true;
    }

    pause() {
        this.playing = false;
    }

    togglePlay() {
        this.playing = !this.playing;
    }
}

customElements.define("video-player", VideoPlayer);

Make range show position slider at current time

The problem

Create a <input type="range"> element that changes its position as the video playing position changes.

The <input type="range"> element should be after the <button> and before the currentTime span like:

</button>
<input type="range">
<span>{{ formatTime(currentTime) }}</span> /

What you need to know

  • The range input can have an initial value, max value, and step size specified like:

    <input type="range" value="0" max="1" step="any">
    
  • The range will have values from 0 to 1. We will need to translate the currentTime to a number between 0 and 1. We can do this with a computed getter property like:

    class SomeElement extends StacheElement {
      static view = `...`;
      static props = {
        // ...
        get percentComplete() {
          return this.currentTime / this.duration;
        }
      };
    }
    
  • Use key:from to update a value from a custom element property like:

    <input value:from="percentComplete">
    

The solution

Update the JavaScript tab to:

import { StacheElement } from "//unpkg.com/can@6/core.mjs";

class VideoPlayer extends StacheElement {
    static view = `
        <video 
            controls
            on:play="this.play()"
            on:pause="this.pause()"
            on:timeupdate="this.updateTimes(scope.element)"
            on:loadedmetadata="this.updateTimes(scope.element)"
        >
            <source src="{{ this.src }}">
        </video>
        <div>
            <button on:click="this.togglePlay()">
                {{# if(this.playing) }} Pause {{ else }} Play {{/ if }}
            </button>
            <input type="range" value="0" max="1" step="any" value:from="this.percentComplete">
            <span>{{ this.formatTime(this.currentTime) }}</span> /
            <span>{{ this.formatTime(this.duration) }} </span>
        </div>
    `;

    static props = {
        src: String,
        playing: Boolean,
        duration: Number,
        currentTime: Number,

        get percentComplete() {
            return this.currentTime / this.duration;
        }
    };

    connected() {
        this.listenTo("playing", function({ value }) {
            if (value) {
                this.querySelector("video").play();
            } else {
                this.querySelector("video").pause();
            }
        });
    }

    updateTimes(videoElement) {
        this.currentTime = videoElement.currentTime || 0;
        this.duration = videoElement.duration;
    }

    formatTime(time) {
        if (time === null || time === undefined) {
            return "--";
        }
        const minutes = Math.floor(time / 60);
        let seconds = Math.floor(time - minutes * 60);
        if (seconds < 10) {
            seconds = "0" + seconds;
        }
        return minutes + ":" + seconds;
    }

    play() {
        this.playing = true;
    }

    pause() {
        this.playing = false;
    }

    togglePlay() {
        this.playing = !this.playing;
    }
}

customElements.define("video-player", VideoPlayer);

Make sliding the range update the current time

The problem

In this section we will:

  • Remove the native controls from the video player. We don’t need them anymore!
  • Make it so when a user moves the range slider, the video position updates.

What you need to know

Similar to when we made the play/pause button play or pause the video, we will want to update the currentTime property and then listen to when currentTime changes and update the <video> element’s currentTime as a side-effect.

This time, we need to translate the sliders values between 0 and 1 to currentTime values. We can do this by creating a percentComplete setter that updates currentTime like:

class VideoPlayer extends StacheElement {
  static view = `...`;
  static props = {
    // ...
    get percentComplete() {
      return this.currentTime / this.duration;
    },
    set percentComplete(newVal) {
      this.currentTime = newVal * this.duration;
    },
    // ...
  };
}

Use key:bind to two-way bind a value to a custom element property:

<input value:bind="someProperty">

The solution

Update the JavaScript tab to:

import { fromAttribute, StacheElement } from "//unpkg.com/can@pre/core.mjs";

class VideoPlayer extends StacheElement {
    static view = `
        <video
            on:play="this.play()"
            on:pause="this.pause()"
            on:timeupdate="this.updateTimes(scope.element)"
            on:loadedmetadata="this.updateTimes(scope.element)"
        >
            <source src="{{ this.src }}" >
        </video>
        <div>
            <button on:click="this.togglePlay()">
                {{# if(this.isPlaying) }} Pause {{ else }} Play {{/ if }}
            </button>
            <input type="range" value="0" max="1" step="any" value:bind="this.percentComplete">
            <span>{{ this.formatTime(this.currentTime) }}</span> /
            <span>{{ this.formatTime(this.duration) }} </span>
        </div>
    `;

    static props = {
        currentTime: Number,
        duration: Number,
        isPlaying: Boolean,
        src: {
            bind: fromAttribute,
            type: String
        },

        get percentComplete() {
            return this.currentTime / this.duration;
        },

        set percentComplete(newVal) {
            this.currentTime = newVal * this.duration;
        }
    };

    connected() {
        this.listenTo("currentTime", ({ value }) => {
            const videoElement = this.querySelector("video");
            if (value !== videoElement.currentTime) {
                videoElement.currentTime = value;
            }
        });
        this.listenTo("isPlaying", ({ value }) => {
            if (value) {
                this.querySelector("video").play();
            } else {
                this.querySelector("video").pause();
            }
        });
    }

    formatTime(time) {
        if (time === null || time === undefined) {
            return "--";
        }
        const minutes = Math.floor(time / 60);
        let seconds = Math.floor(time - minutes * 60);
        if (seconds < 10) {
            seconds = "0" + seconds;
        }
        return minutes + ":" + seconds;
    }

    play() {
        this.isPlaying = true;
    }

    pause() {
        this.isPlaying = false;
    }

    togglePlay() {
        this.isPlaying = !this.isPlaying;
    }

    updateTimes(videoElement) {
        this.currentTime = videoElement.currentTime || 0;
        this.duration = videoElement.duration;
    }
}

customElements.define("video-player", VideoPlayer);

Result

When finished, you should see something like the following JS Bin:

See the Pen CanJS 6.0 Video Player - Final by Bitovi (@bitovi) on CodePen.

CanJS is part of DoneJS. Created and maintained by the core DoneJS team and Bitovi. Currently 6.0.1.

On this page

Get help

  • Chat with us
  • File an issue
  • Ask questions
  • Read latest news