Neste guia, o senhor aprenderá a criar um player de vídeo personalizado usando o <video> e o elemento CanJS. O
player de vídeo personalizado será:

  • Ter botões personalizados de reprodução e pausa.
  • Mostrar a hora atual e a duração do vídeo.
  • Tenha um <input type="range"> que pode ajustar a posição do vídeo.

O jogador final se parece com o senhor:

As seções a seguir estão divididas nas seguintes partes:

  • O problema – Uma descrição do que a seção está tentando realizar.
  • O que o senhor precisa saber – APIs do navegador ou do CanJS que são úteis para resolver o problema.
  • A solução – A solução para o problema.

Instalação

Comece este tutorial bifurcando o seguinte CodePen:

Clique no botão Edit in CodePen . O CodePen será aberto em uma nova janela. Clique no botão Fork .

Este CodePen:

  • Cria um <video> que carrega um vídeo. Clique com o botão direito do mouse e selecione “Show controls” para ver os controles do vídeo.
  • Carrega a biblioteca de elementos personalizados do CanJS: Componente.

O problema

Nesta seção, veremos:

  • Criar um <video-player> personalizado que recebe um elemento src e cria um atributo <video> elemento
    dentro dele mesmo. Devemos ser capazes de criar o vídeo da seguinte forma:

    <video-player src:raw="http://bit.ly/can-tom-n-jerry">
    </video-player>
  • A incorporação <video> incorporado deve ter os controles nativos ativados.

Quando concluído, o resultado terá exatamente a mesma aparência do jogador quando o senhor começou. A
única diferença é que estaremos usando um <video-player> personalizado no elemento HTML
em vez da guia nativa <video> elemento.

O que o senhor precisa saber

Para configurar um aplicativo (ou widget) CanJS básico, o senhor define um elemento personalizado em JavaScript e
usar o elemento personalizado na seção HTML.

Para definir um elemento personalizado, estenda o Componente com um tag
que corresponda ao nome do seu elemento personalizado. Por exemplo:

Component.extend({
    tag: "video-player"
})

Em seguida, o senhor pode usar essa tag em sua página HTML:

<video-player></video-player>

Mas isso não faz nada… ainda. Os componentes adicionam seu próprio HTML por meio de seus visualização
Propriedade:

Component.extend({
    tag: "video-player",
    view: `<h2>I am a player!</h2>`
});

Um componente visualização é renderizada com sua ViewModel. Por exemplo, podemos criar um <video> exibição "http://bit.ly/can-tom-n-jerry" definindo um src na propriedade ViewModel e usá-la no visualização como:

Component.extend({
    tag: "video-player",
    view: `
        <video>
            <source src="https://davidwalsh.name/{{src}}"/>
        </video>
    `,
    ViewModel: {
        src: {type: "string", default: "http://bit.ly/can-tom-n-jerry"}
    }
});

Mas nós queremos que o <video-player> tomem uma src e usá-lo para o
<source>‘s src. Por exemplo, se
quiséssemos que o vídeo fosse reproduzido "http://dl3.webmfiles.org/big-buck-bunny_trailer.webm" em vez de "http://bit.ly/can-tom-n-jerry", nós o faríamos:

  1. Atualização <video-player> para passar "http://dl3.webmfiles.org/big-buck-bunny_trailer.webm" com toChild:raw:
    <video-player src:raw="http://dl3.webmfiles.org/big-buck-bunny_trailer.webm"/>
  2. Atualize o ViewModel para definir um src propriedade como:
    Component.extend({
        tag: "video-player",
        view: `
            <video>
                <source src="https://davidwalsh.name/{{src}}"/> {{!👀}}
            </video>
        `,
        ViewModel: {
            src: "string"
        }
    });

Finalmente, para ter um <video> mostre o elemento nativo nativo, adicione um controls
como:

<video controls>

A solução

Atualize o JS tab para:

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

Component.extend({                        //👀
    tag: "video-player",                  //👀
    view: `                              {{!👀}}
        <video controls>                 {{!👀}}
            <source src="https://davidwalsh.name/{{src}}"/>      {{!👀}}
        </video>                         {{!👀}}
    `,                                    //👀
    ViewModel: {                          //👀
        src: "string",                    //👀
    }                                     //👀
});                                       //👀

Atualize o HTML para:

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


Fazer com que o botão de reprodução / pausa mude conforme o vídeo é reproduzido e pausado

O problema

Quando o vídeo é reproduzido ou pausado usando os controles nativos, queremos alterar o conteúdo de um <button>
para dizer “Play” ou “Pausa”.

Quando o vídeo for reproduzido, o botão deverá dizer “Pause” (Pausar).
Quando o vídeo é pausado, o botão deve dizer “Play” (Reproduzir).

Queremos que o botão esteja dentro de um <div> após o elemento de vídeo, como:

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

O que o senhor precisa saber

  • Para alterar o conteúdo HTML da página, use {{#if(expression)}} e {{else}} como:

    <button>{{#if(playing)}} Pause {{else}} Play {{/if}}</button>
  • O ver responde a valores no ViewModel. Para criar um boolean no arquivo ViewModel do:

    ViewModel: {
        // ...
        playing: "boolean",
    }
  • Os métodos podem ser usados para alterar o ViewModel. A seguir, podemos criar métodos que alteram o playing valor:

    ViewModel: {
        // ...
        play() {
            this.playing = true;
        },
        pause() {
            this.playing = false;
        },
    }
  • O senhor pode ouvir eventos no DOM com on:event. Por exemplo, o seguinte poderia
    ouvir um clique em um <div> e chamar o doSomething():

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

    <video> Os elementos têm uma variedade de eventosincluindo play e
    pausa eventos que são emitidos quando o vídeo é reproduzido ou pausado.

A solução

Atualize o JavaScript tab para:

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

Component.extend({
    tag: "video-player",
    view: `
        <video controls
            on:play="play()"                                      {{!👀}}
            on:pause="pause()">                                   {{!👀}}
            <source src="https://davidwalsh.name/{{src}}"/>
        </video>
        <div>                                                     {{!👀}}
            <button>                                              {{!👀}}
                {{#if(playing)}} Pause {{else}} Play {{/if}}      {{!👀}}
            </button>                                             {{!👀}}
        </div>                                                    {{!👀}}
    `,
    ViewModel: {
        src: "string",
        playing: "boolean",                                        //👀

        play() {                                                   //👀
            this.playing = true;                                   //👀
        },                                                         //👀
        pause() {                                                  //👀
            this.playing = false;                                  //👀
        },                                                         //👀
    }
});


Fazer com que clicar no botão reproduzir/pausar reproduza ou pause o vídeo

O problema

Quando o play/pause <button> que criamos na seção anterior for clicado, queremos
reproduzir ou pausar o vídeo.

O que o senhor precisa saber

O CanJS prefere gerenciar o estado do seu aplicativo em ViewModel. O <video> tem um estado, como
se o vídeo for playing. Quando o play/pause for clicado, queremos atualizar o estado
do botão ViewModel e ter o ViewModel atualizar o estado do player de vídeo como um efeito colateral.

O que isso significa é que, em vez de algo como:

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

Atualizamos o estado como o senhor:

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

E ouça quando o playing muda e atualize o video como:

viewModel.listenTo("playing", function(event, isPlaying) {
    if ( isPlaying ) {
        videoElement.play()
    } else {
        videoElement.pause()
    }
})

Isso significa que o senhor precisa:

  1. Ouça quando o <button> é clicado e chama um método ViewModel que atualiza o playing estado.
  2. Ouça quando o playing muda e atualiza o estado do <video> .

O senhor já sabe tudo o que precisa saber para a etapa #1. (Faça com que o botão chame um togglePlay com o método on:click="togglePlay()" e faça o togglePlay() alterne o estado do método playing ).

Por etapa #2, o senhor precisa usar o connectedCallback gancho do ciclo de vida. Esse
dá ao senhor acesso ao elemento do componente e é um bom lugar para fazer efeitos colaterais. Seu uso parece
assim:

ViewModel: {
    // ...
    connectedCallback(element) {
        // perform mischief
    }
}

connectedCallback é chamado quando o componente element do componente estiver na página. O senhor pode usar
listenTo para ouvir as alterações no ViewModele
realizam efeitos colaterais. O seguinte escuta quando playing muda:

ViewModel: {
    // ...
    connectedCallback(element) {
        this.listenTo("playing", function(event, isPlaying) {

        })
    }
}

Use querySelector para obter o <video> do elemento <video-player> como:

element.querySelector("video")

<video> os elementos têm um .play() e .pause() métodos que podem iniciar e parar um vídeo.

A solução

Atualize o JavaScript tab para:

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

Component.extend({
    tag: "video-player",
    view: `
        <video controls
            on:play="play()"
            on:pause="pause()">
            <source src="https://davidwalsh.name/{{src}}"/>
        </video>
        <div>
            <button on:click="togglePlay()">                        {{!👀}}
                {{#if(playing)}} Pause {{else}} Play {{/if}}
            </button>
        </div>
    `,
    ViewModel: {
        src: "string",
        playing: "boolean",

        play() {
            this.playing = true;
        },
        pause() {
            this.playing = false;
        },
        togglePlay() {                                               //👀
            this.playing = !this.playing;                            //👀
        },                                                           //👀

        connectedCallback(element) {                                 //👀
            this.listenTo("playing", function(event, isPlaying) {    //👀
                if (isPlaying) {                                     //👀
                    element.querySelector("video").play();           //👀
                } else {                                             //👀
                    element.querySelector("video").pause();          //👀
                }                                                    //👀
            });                                                      //👀
        }                                                            //👀
    }
});


Mostrar a hora atual e a duração

O problema

Mostrar a hora atual e a duração do elemento de vídeo. A hora e a duração devem ser
formatados como: mm:SS. Eles devem ser apresentados em dois intervalos, como:

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

O que o senhor precisa saber

  1. Os métodos podem ser usados para formatar valores em can-stache. Por exemplo, o senhor pode colocar valores em maiúsculas como este:

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

    Com um método como:

    ViewModel: {
        // ...
        upper(value) {
            return value.toString().toUpperCase();
        }
    }

    O seguinte pode ser usado para formatar a hora:

    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;
    }
  2. O tempo é dado como um número. Use o seguinte para criar uma propriedade de número em
    o ViewModel:

    ViewModel: {
        // ...
        duration: "number",
        currentTime: "number"
    }
  3. <video> os elementos emitem um evento loadmetadata quando souberem a duração
    o vídeo é. Eles também emitem um evento timeupdate quando a posição atual de reprodução do vídeo
    muda.

    • videoElement.duration lê a duração de um vídeo.
    • videoElement.currentTime lê a posição de reprodução atual de um vídeo.
  4. O senhor pode obter o elemento em um stache on:event com o scope.element como:

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

A solução

Atualize o JavaScript tab para:

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

Component.extend({
    tag: "video-player",
    view: `
        <video controls
            on:play="play()"
            on:pause="pause()"
            on:timeupdate="updateTimes(scope.element)"           {{!👀}}
            on:loadedmetadata="updateTimes(scope.element)">      {{!👀}}
            <source src="https://davidwalsh.name/{{src}}"/>
        </video>
        <div>
            <button on:click="togglePlay()">
                {{#if(playing)}} Pause {{else}} Play {{/if}}
            </button>
            <span>{{formatTime(currentTime)}}</span> /           {{!👀}}
            <span>{{formatTime(duration)}} </span>               {{!👀}}
        </div>
    `,
    ViewModel: {
        src: "string",
        playing: "boolean",
        duration: "number",                                       //👀
        currentTime: "number",                                    //👀

        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;
        },

        connectedCallback(element) {
            this.listenTo("playing", function(event, isPlaying) {
                if (isPlaying) {
                    element.querySelector("video").play();
                } else {
                    element.querySelector("video").pause();
                }
            });
        }
    }
});


Fazer com que o intervalo mostre o controle deslizante de posição no momento atual

O problema

Criar um <input type="range"/> que muda sua posição conforme
a posição de reprodução do vídeo muda.

O <input type="range"/> deve estar após o elemento <button> e antes do
currentTime span like:

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


O que o senhor precisa saber

  • A entrada do intervalo pode ter um valor inicial, um valor máximo e um tamanho de etapa
    especificados como:

    <input type="range" value="0" max="1" step="any"/>
  • O intervalo terá valores de 0 a 1. Precisaremos traduzir o currentTime para
    um número entre 0 e 1. Podemos fazer isso com um propriedade getter computada como:

    ViewModel: {
        // ...
        get percentComplete() {
            return this.currentTime / this.duration;
        },
    }
  • Use key:from para atualizar um valor de um ViewModel propriedade como:

    <input value:from="percentComplete"/>

A solução

Atualize o JavaScript tab para:

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

Component.extend({
    tag: "video-player",
    view: `
        <video controls
            on:play="play()"
            on:pause="pause()"
            on:timeupdate="updateTimes(scope.element)"
            on:loadedmetadata="updateTimes(scope.element)">
            <source src="https://davidwalsh.name/{{src}}"/>
        </video>
        <div>
            <button on:click="togglePlay()">
                {{#if(playing)}} Pause {{else}} Play {{/if}}
            </button>
            <input type="range" value="0" max="1" step="any"        {{!👀}}
                    value:from="percentComplete"/>                  {{!👀}}
            <span>{{formatTime(currentTime)}}</span> /
            <span>{{formatTime(duration)}} </span>
        </div>
    `,
    ViewModel: {
        src: "string",
        playing: "boolean",
        duration: "number",
        currentTime: "number",

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

        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;
        },

        connectedCallback(element) {
            this.listenTo("playing", function(event, isPlaying) {
                if (isPlaying) {
                    element.querySelector("video").play();
                } else {
                    element.querySelector("video").pause();
                }
            });
        }
    }
});


Fazer com que o deslizamento do intervalo atualize a hora atual

O problema

Nesta seção, veremos:

  • Remova os controles nativos do player de vídeo. Não precisamos mais deles!
  • Faça com que, quando um usuário mover o controle deslizante de intervalo, a posição do vídeo seja atualizada.

O que o senhor precisa saber

Semelhante a quando nós fizemos o botão reproduzir/pausar reproduzir ou pausar o vídeo, vamos querer atualizar o
currentTime e, em seguida, ouvir quando o currentTime muda e atualiza a propriedade <video>
do elemento currentTime como um efeito colateral.

Desta vez, precisamos traduzir os valores dos controles deslizantes entre 0 e 1 para currentTime
valores. Podemos fazer isso criando um percentComplete setter que atualiza currentTime como:

ViewModel: {
    // ...
    get percentComplete() {
        return this.currentTime / this.duration;
    },
    set percentComplete(newVal) {
        this.currentTime = newVal * this.duration;
    },
    // ...
}

Use key:bind para vincular de forma bidirecional um valor a um ViewModel propriedade:

<input value:bind="someViewModelProperty"/>

A solução

Atualize o JavaScript tab para:

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

Component.extend({
    tag: "video-player",
    view: `
        <video                                                          {{!👀}}
            on:play="play()"
            on:pause="pause()"
            on:timeupdate="updateTimes(scope.element)"
            on:loadedmetadata="updateTimes(scope.element)">
            <source src="https://davidwalsh.name/{{src}}"/>
        </video>
        <div>
            <button on:click="togglePlay()">
                {{#if(playing)}} Pause {{else}} Play {{/if}}
            </button>
            <input type="range" value="0" max="1" step="any"
                    value:bind="percentComplete"/>                      {{!👀}}
            <span>{{formatTime(currentTime)}}</span> /
            <span>{{formatTime(duration)}} </span>
        </div>
    `,
    ViewModel: {
        src: "string",
        playing: "boolean",
        duration: "number",
        currentTime: "number",

        get percentComplete() {
            return this.currentTime / this.duration;
        },
        set percentComplete(newVal) {                                    //👀
            this.currentTime = newVal * this.duration;                   //👀
        },                                                               //👀

        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;
        },

        connectedCallback(element) {
            this.listenTo("playing", function(event, isPlaying) {
                if (isPlaying) {
                    element.querySelector("video").play();
                } else {
                    element.querySelector("video").pause();
                }
            });
            this.listenTo("currentTime", function(event, currentTime) {  //👀
                const videoElement = element.querySelector("video");     //👀
                if (currentTime !== videoElement.currentTime) {          //👀
                    videoElement.currentTime = currentTime;              //👀
                }                                                        //👀
            });                                                          //👀
        }
    }
});


Resultado

Quando terminar, o senhor deverá ver algo parecido com o seguinte JS Bin:

Justin Meyer

Sobre Justin Meyer

Justin Meyer é colaborador de CanJS (uma estrutura de aplicativo da Web), StealJS (um carregador de módulos) e DoneJS (uma pia de cozinha). Ele também é um dos fundadores do Bitovi (uma consultoria na Web). Como escritor de artigos autobiográficos, ele sabe usar seus dois filhos, seu amor por livros de história e hobbies como correr para parecer mais humano. Mas será que ele é realmente …