queenvictoria / ember-videojs

MIT License
5 stars 11 forks source link

Ember 4 / Glimmer support #13

Open nova-alex opened 1 year ago

nova-alex commented 1 year ago

Hi! I've been playing around with this addon and it's rather nice. Unfortunately it's a classic component and although I did manage to get it working on its own, I wasn't able to import into and Ember 4 app.

I'm not great with creating addons and all that stuff but I do have the js and hbs files here which I'd like to offer to anyone who might like to use them.

videojs-player.hbs

<video class="video-js" {{did-insert this.didInsertHandler}} {{did-update this.didUpdateHandler}} {{will-destroy this.willDestroyHandloer}} autoplay={{this.autoplay}} controls={{this.controls}} loop={{this.loop}} muted={{this.muted}} playsinline={{this.playsinline}} preload="auto" poster={{this.poster}} data-setup="{}" crossorigin={{this.crossorigin}} tabindex="0">
  {{#if this.src}}
    <source src={{this.src}} type={{this.type}}>
  {{/if}}

  {{#if this.sources}}
    {{#each this.sources as |item|}}
      <source src={{item.src}} type={{item.type}}>
    {{/each}}
  {{/if}}

  {{#if this.textTrack}}
    <track kind="captions" src={{this.textTrack}} srclang="en" label="English" type="text/vtt" default>
  {{/if}}

  {{#if this.textTracks}}
    {{#each this.textTracks as |textTrack|}}
      <track kind={{textTrack.kind}} src={{textTrack.src}} srclang={{textTrack.language}} label={{textTrack.label}} type={{textTrack.type}} default>
    {{/each}}
  {{/if}}

  <p class="vjs-no-js">
    To view this video please enable JavaScript, and consider upgrading to a web browser that
    <a href="https://videojs.com/html5-video-support/" target="_blank" rel="noopener noreferrer">supports HTML5 video</a>
  </p>
</video>

videojs-player.js

import Component from '@glimmer/component';
import videojs from 'video.js';
// import { addObserver, removeObserver } from '@ember/object/observers';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

// https://github.com/IvyApp/ivy-videojs/blob/0bb2e1513bec874f9ce9cf48ffd3f6996623553b/addon/components/ivy-videojs.js
export default class VideojsPlayer extends Component {
  @tracked player = null;

  _defaults = {
    autoplay: false,
    controls: true,
    fluid: false,
    loop: false,
    muted: true,
    height: 520,
    width: 824,
    html5: true,
  };

  // _defaults
  get autoplay() {
    return this.args.autoplay || this._defaults.autoplay;
  }
  get controls() {
    return this.args.controls || this._defaults.controls;
  }
  get fluid() {
    return this.args.fluid || this._defaults.fluid;
  }
  get loop() {
    return this.args.loop || this._defaults.loop;
  }
  get muted() {
    return this.args.muted || this._defaults.muted;
  }
  get height() {
    return this.args.height || this._defaults.height;
  }
  get width() {
    return this.args.width || this._defaults.width;
  }
  get html5() {
    return this.args.html5 || this._defaults.html5;
  }

  get src() {
    return this.args.src;
  }

  get sources() {
    return this.args.sources;
  }

  get textTrack() {
    return this.args.textTrack;
  }

  get textTracks() {
    return this.args.textTracks;
  }

  get ready() {
    return this.args.ready;
  }

  playerEvents = null;

  /**
   * The set of video.js player events (and associated actions) to be set up
   * inside `didInsertElement`. If you need to respond to custom video.js
   * player events, such as those emitted by a plugin, you should do so by
   * calling `sendActionOnPlayerEvent` inside your `ready` handler.
   *
   * ```javascript
   * import Ember from 'ember';
   *
   * export default Ember.Controller.extend({
   *   actions: {
   *     ready(player, component) {
   *       component.sendActionOnPlayerEvent(player, 'actionName', 'eventName');
   *     }
   *   }
   * });
   * ```
   *
   * The above would send the `actionName` action in response to an `eventName`
   * event from the player.
   *
   * @property playerEvents
   * @type Object
   * @private
   */
  constructor(...args) {
    super(...args);

    this.playerEvents = [
      'abort',
      'canplay',
      'canplaythrough',
      'durationchange',
      'emptied',
      'ended',
      'error',
      'loadeddata',
      'loadedmetadata',
      'loadstart',
      'pause',
      'play',
      'playing',
      'progress',
      'ratechange',
      'resize',
      'seeked',
      'seeking',
      'stalled',
      'suspend',
      'timeupdate',
      'useractive',
      'userinactive',
      'volumechange',
      'waiting',
      'click',
      'tap',
    ];
  }

  @action
  didInsertHandler(element) {
    const playerOptions = {};
    playerOptions.html5 = this.html5 || {};

    if (this.liveui) {
      playerOptions.liveui = true;
    }

    let player = videojs(element, playerOptions);

    if (this.height) {
      player.height(this.height);
    }

    if (this.width) {
      player.width(this.width);
    }

    if (this.fluid) {
      player.fluid(this.fluid);
    }

    if (this.aspectRatio) {
      player.aspectRatio(this.aspectRatio);
    }

    // // Register plugins
    // // Get global plugins from config.
    // if (this['vr-projection']) {
    //   if (typeof player.vr === 'function') {
    //     this.set('crossorigin', 'anonymous');
    //     player.vr({ projection: this['vr-projection'] });
    //   } else {
    //     throw new EmberError(
    //       "It looks like you are trying to play a VR video without the videojs-vr library. Please `npm install --save-dev videojs-vr` and add `app.import('node_modules/videojs-vr/dist/videojs-vr.min.js');` to your ember-cli-build.js file."
    //     );
    //   }
    // }

    // https://github.com/IvyApp/ivy-videojs/blob/master/addon/components/ivy-videojs-player.js
    player.ready(() => {
      // Set up event listeners defined in `playerEvents`.
      let playerEvents = this.playerEvents;
      if (playerEvents) {
        playerEvents.forEach((val) => {
          this.sendActionOnPlayerEvent(player, val);
        });
      }

      // Let the outside world know that we're ready.
      if (this.ready && typeof this.ready === 'function') {
        this.ready(player, this);
      }
    });

    this.player = player;
  }

  @action
  didUpdateHandler(element) {
    let player = videojs(element);
    player.pause();
    player.src(this.src);
    player.load();
  }

  @action
  willDestroyHandler() {
    this.player.dispose();
  }

  /**
   * Sets up a listener that sends an action on a video.js player event.
   *
   * @method sendActionOnPlayerEvent
   * @param {Player} player the video.js player instance
   * @param {String} action the action name to be sent
   * @param {String} playerEvent the player event name to listen for
   */
  sendActionOnPlayerEvent(player, action) {
    const listenerFunction = (...args) => {
      console.log('Hey Hey', action);
      if (this.args[action]) {
        this.args[action](player, this, ...args);
      }
    };

    this._onPlayerEvent(player, action, listenerFunction);
  }

  /**
   * Sets the value of a property on a video.js player. If the property is
   * already equal to the given value, no change is made.
   *
   * @method setPlayerProperty
   * @param {Player} player the video.js player instance
   * @param {String} playerProperty the name of the property to set
   * @param {Object} value the value to set
   */
  setPlayerProperty(player, playerProperty, value) {
    const propertyMethod = player[playerProperty];
    if (propertyMethod) {
      const previousValue = propertyMethod.call(player);
      if (previousValue !== value) {
        propertyMethod.call(player, value);
      }
    }
  }

  // _addPlayerObserver(property, target, observer) {
  //   if (this.isDestroying) {
  //     return;
  //   }
  //   addObserver(this, property, target, observer);

  //   this.one('willDestroyElement', this, function () {
  //     removeObserver(this, property, target, observer);
  //   });
  // }

  _onPlayerEvent(player, eventName, listenerFunction) {
    player.on(eventName, listenerFunction);
  }
}

It relies on https://github.com/emberjs/ember-render-modifiers and can be used with @ properties

<VideojsPlayer @src="https://vjs.zencdn.net/v/oceans.mp4" @type="video/mp4"
    @poster="https://vjs.zencdn.net/v/oceans.png" @autoplay="true"
    @timeupdate={{this.timeupdate}} @ready={{this.ready}}/>

I hope this information is helpful. Currently I'm using it as an in-repo addon and I've commented out the observer portion because it can probably be done differently (i.e. using render helpers outside of this component) and the VR feature just cuz I'm not using it.

nova-alex commented 1 year ago

This removes the jQuery dependency and simplifies the action handling a bit as well.