opentok / opentok-react

React components for OpenTok.js
https://www.npmjs.com/package/opentok-react
MIT License
107 stars 105 forks source link

Subscriber breaks when insertDefaultUI = false is used #38

Open shripad-agashe opened 6 years ago

shripad-agashe commented 6 years ago

I guess the fix is similar to issue mentioned below. https://github.com/opentok/opentok-react/issues/3

MiguelArmendariz commented 6 years ago

Do you want to set the subscriber/publisher controls to false?, I mean to hide the controls?

shripad-agashe commented 6 years ago

No. I want to be able to supply a custom UI for subscriber. The basic use case is to make it change size based on who is speaking etc. The fix is exactly like the one mentioned in issue above.

varseb commented 6 years ago

Hello, I am also having this issue. I want to get the video element triggered on videoElementCreated event when insertDefaultUI = false Any news about this issue? Publisher is properly working.

danlupascu commented 5 years ago

I am having the same issue. Any news?

Edit: I don't have enough time to wait for updates, so I've modified the OTSubscriber component until this bug is fixed.

Here's my component in case someone else is having the same issue:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import uuid from 'uuid';

export class OTSubscriber extends Component {
  constructor(props, context) {
    super(props);

    this.state = {
      subscriber: null,
      stream: props.stream || context.stream || null,
      session: props.session || context.session || null,
    };
  }

  componentDidMount() {
    this.createSubscriber();
  }

  componentDidUpdate(prevProps, prevState) {
    const cast = (value, Type, defaultValue) => (value === undefined ? defaultValue : Type(value));

    const updateSubscriberProperty = (key) => {
      const previous = cast(prevProps.properties[key], Boolean, true);
      const current = cast(this.props.properties[key], Boolean, true);
      if (previous !== current) {
        this.state.subscriber[key](current);
      }
    };

    updateSubscriberProperty('subscribeToAudio');
    updateSubscriberProperty('subscribeToVideo');

    if (prevState.session !== this.state.session || prevState.stream !== this.state.stream) {
      this.destroySubscriber(prevState.session);
      this.createSubscriber();
    }
  }

  componentWillUnmount() {
    this.destroySubscriber();
  }

  getSubscriber() {
    return this.state.subscriber;
  }

  createSubscriber() {
    if (!this.state.session || !this.state.stream) {
      this.setState({ subscriber: null });
      return;
    }

    let container = null;

    const { properties } = this.props;

    if (!properties || properties.insertDefaultUI || properties.insertDefaultUI === undefined) {
      container = document.createElement('div');
      container.setAttribute('class', 'OTSubscriberContainer');
      this.node.appendChild(container);
    }

    this.subscriberId = uuid();
    const { subscriberId } = this;

    const subscriber = this.state.session.subscribe(
        this.state.stream,
        container,
        this.props.properties,
        (err) => {
          if (subscriberId !== this.subscriberId) {
            // Either this subscriber has been recreated or the
            // component unmounted so don't invoke any callbacks
            return;
          }
          if (err && typeof this.props.onError === 'function') {
            this.props.onError(err);
          } else if (!err && typeof this.props.onSubscribe === 'function') {
            this.props.onSubscribe();
          }
        },
    );

    if (
        this.props.eventHandlers &&
        typeof this.props.eventHandlers === 'object'
    ) {
      subscriber.on(this.props.eventHandlers);
    }

    this.setState({ subscriber });
  }

  destroySubscriber(session = this.props.session) {
    delete this.subscriberId;

    if (this.state.subscriber) {
      if (
          this.props.eventHandlers &&
          typeof this.props.eventHandlers === 'object'
      ) {
        this.state.subscriber.once('destroyed', () => {
          this.state.subscriber.off(this.props.eventHandlers);
        });
      }

      if (session) {
        session.unsubscribe(this.state.subscriber);
      }
    }
  }

  render() {
    return <div ref={(node) => { this.node = node; }} />;
  }
}

OTSubscriber.propTypes = {
  stream: PropTypes.shape({
    streamId: PropTypes.string,
  }),
  session: PropTypes.shape({
    subscribe: PropTypes.func,
    unsubscribe: PropTypes.func,
  }),
  properties: PropTypes.object, // eslint-disable-line react/forbid-prop-types
  eventHandlers: PropTypes.objectOf(PropTypes.func),
  onSubscribe: PropTypes.func,
  onError: PropTypes.func,
};

OTSubscriber.defaultProps = {
  stream: null,
  session: null,
  properties: {},
  eventHandlers: null,
  onSubscribe: null,
  onError: null,
};

OTSubscriber.contextTypes = {
  stream: PropTypes.shape({
    streamId: PropTypes.string,
  }),
  session: PropTypes.shape({
    subscribe: PropTypes.func,
    unsubscribe: PropTypes.func,
  }),
};
session: props.session || context.session || null,
    };
  }

  componentDidMount() {
    this.createSubscriber();
  }

  componentDidUpdate(prevProps, prevState) {
    const cast = (value, Type, defaultValue) => (value === undefined ? defaultValue : Type(value));

    const updateSubscriberProperty = (key) => {
      const previous = cast(prevProps.properties[key], Boolean, true);
      const current = cast(this.props.properties[key], Boolean, true);
      if (previous !== current) {
        this.state.subscriber[key](current);
      }
    };

    updateSubscriberProperty('subscribeToAudio');
    updateSubscriberProperty('subscribeToVideo');

    if (prevState.session !== this.state.session || prevState.stream !== this.state.stream) {
      this.destroySubscriber(prevState.session);
      this.createSubscriber();
    }
  }

  componentWillUnmount() {
    this.destroySubscriber();
  }

  getSubscriber() {
    return this.state.subscriber;
  }

  createSubscriber() {
    if (!this.state.session || !this.state.stream) {
      this.setState({ subscriber: null });
      return;
    }

    let container = null;

    if (!this.props.properties || this.props.properties.insertDefaultUI) {
      container = document.createElement('div');
      container.setAttribute('class', 'OTSubscriberContainer');
      this.node.appendChild(container);
    }

    this.subscriberId = uuid();
    const { subscriberId } = this;

    const subscriber = this.state.session.subscribe(
        this.state.stream,
        container,
        this.props.properties,
        (err) => {
          if (subscriberId !== this.subscriberId) {
            // Either this subscriber has been recreated or the
            // component unmounted so don't invoke any callbacks
            return;
          }
          if (err && typeof this.props.onError === 'function') {
            this.props.onError(err);
          } else if (!err && typeof this.props.onSubscribe === 'function') {
            this.props.onSubscribe();
          }
        },
    );

    if (
        this.props.eventHandlers &&
        typeof this.props.eventHandlers === 'object'
    ) {
      subscriber.on(this.props.eventHandlers);
    }

    this.setState({ subscriber });
  }

  destroySubscriber(session = this.props.session) {
    delete this.subscriberId;

    if (this.state.subscriber) {
      if (
          this.props.eventHandlers &&
          typeof this.props.eventHandlers === 'object'
      ) {
        this.state.subscriber.once('destroyed', () => {
          this.state.subscriber.off(this.props.eventHandlers);
        });
      }

      if (session) {
        session.unsubscribe(this.state.subscriber);
      }
    }
  }

  render() {
    return <div ref={(node) => { this.node = node; }} />;
  }
}

OTSubscriber.propTypes = {
  stream: PropTypes.shape({
    streamId: PropTypes.string,
  }),
  session: PropTypes.shape({
    subscribe: PropTypes.func,
    unsubscribe: PropTypes.func,
  }),
  properties: PropTypes.object, // eslint-disable-line react/forbid-prop-types
  eventHandlers: PropTypes.objectOf(PropTypes.func),
  onSubscribe: PropTypes.func,
  onError: PropTypes.func,
};

OTSubscriber.defaultProps = {
  stream: null,
  session: null,
  properties: {},
  eventHandlers: null,
  onSubscribe: null,
  onError: null,
};

OTSubscriber.contextTypes = {
  stream: PropTypes.shape({
    streamId: PropTypes.string,
  }),
  session: PropTypes.shape({
    subscribe: PropTypes.func,
    unsubscribe: PropTypes.func,
  }),
};
diegodurs commented 4 years ago

Would love to see this fixe being merged.

Because the fix above seems old and has a duplication on the copy/paste (it should be around 180 lines) here is my hot fix where the only change is

    let container;

    if (!this.props.properties || this.props.properties.insertDefaultUI) {
      container = document.createElement('div');
      container.setAttribute('class', 'OTSubscriberContainer');
      this.node.appendChild(container);
    }

Complete file:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import uuid from 'uuid';

export default class OTSubscriber extends Component {
  constructor(props, context) {
    super(props);

    this.state = {
      subscriber: null,
      stream: props.stream || context.stream || null,
      session: props.session || context.session || null,
      currentRetryAttempt: 0,
    };
    this.maxRetryAttempts = props.maxRetryAttempts || 5;
    this.retryAttemptTimeout = props.retryAttemptTimeout || 1000;
  }

  componentDidMount() {
    this.createSubscriber();
  }

  componentDidUpdate(prevProps, prevState) {
    const cast = (value, Type, defaultValue) => (value === undefined ? defaultValue : Type(value));

    const updateSubscriberProperty = (key) => {
      const previous = cast(prevProps.properties[key], Boolean, true);
      const current = cast(this.props.properties[key], Boolean, true);
      if (previous !== current) {
        this.state.subscriber[key](current);
      }
    };

    updateSubscriberProperty('subscribeToAudio');
    updateSubscriberProperty('subscribeToVideo');

    if (prevState.session !== this.state.session || prevState.stream !== this.state.stream) {
      this.destroySubscriber(prevState.session);
      this.createSubscriber();
    }
  }

  componentWillUnmount() {
    this.destroySubscriber();
  }

  getSubscriber() {
    return this.state.subscriber;
  }

  createSubscriber() {
    if (!this.state.session || !this.state.stream) {
      this.setState({ subscriber: null });
      return;
    }

    let container;

    if (!this.props.properties || this.props.properties.insertDefaultUI) {
      container = document.createElement('div');
      container.setAttribute('class', 'OTSubscriberContainer');
      this.node.appendChild(container);
    }

    this.subscriberId = uuid();
    const { subscriberId } = this;

    const subscriber = this.state.session.subscribe(
            this.state.stream,
            container,
            this.props.properties,
            (err) => {
              if (subscriberId !== this.subscriberId) {
                    // Either this subscriber has been recreated or the
                    // component unmounted so don't invoke any callbacks
                return;
              }
              if (err &&
                this.props.retry &&
                this.state.currentRetryAttempt < (this.maxRetryAttempts - 1)) {
                // Error during subscribe function
                this.handleRetrySubscriber();
                // If there is a retry action, do we want to execute the onError props function?
                // return;
              }
              if (err && typeof this.props.onError === 'function') {
                this.props.onError(err);
              } else if (!err && typeof this.props.onSubscribe === 'function') {
                this.props.onSubscribe();
              }
            },
        );

    if (
            this.props.eventHandlers &&
            typeof this.props.eventHandlers === 'object'
        ) {
      subscriber.on(this.props.eventHandlers);
    }

    this.setState({ subscriber });
  }

  handleRetrySubscriber() {
    setTimeout(() => {
      this.setState(state => ({
        currentRetryAttempt: state.currentRetryAttempt + 1,
        subscriber: null,
      }));
      this.createSubscriber();
    }, this.retryAttemptTimeout);
  }

  destroySubscriber(session = this.props.session) {
    delete this.subscriberId;

    if (this.state.subscriber) {
      if (
                this.props.eventHandlers &&
                typeof this.props.eventHandlers === 'object'
            ) {
        this.state.subscriber.once('destroyed', () => {
          this.state.subscriber.off(this.props.eventHandlers);
        });
      }

      if (session) {
        session.unsubscribe(this.state.subscriber);
      }
    }
  }

  render() {
    const { className, style } = this.props;
    return <div className={className} style={style} ref={(node) => { this.node = node; }} />;
  }
}

OTSubscriber.propTypes = {
  stream: PropTypes.shape({
    streamId: PropTypes.string,
  }),
  session: PropTypes.shape({
    subscribe: PropTypes.func,
    unsubscribe: PropTypes.func,
  }),
  className: PropTypes.string,
  style: PropTypes.object, // eslint-disable-line react/forbid-prop-types
  properties: PropTypes.object, // eslint-disable-line react/forbid-prop-types
  retry: PropTypes.bool,
  maxRetryAttempts: PropTypes.number,
  retryAttemptTimeout: PropTypes.number,
  eventHandlers: PropTypes.objectOf(PropTypes.func),
  onSubscribe: PropTypes.func,
  onError: PropTypes.func,
};

OTSubscriber.defaultProps = {
  stream: null,
  session: null,
  className: '',
  style: {},
  properties: {},
  retry: false,
  maxRetryAttempts: 5,
  retryAttemptTimeout: 1000,
  eventHandlers: null,
  onSubscribe: null,
  onError: null,
};

OTSubscriber.contextTypes = {
  stream: PropTypes.shape({
    streamId: PropTypes.string,
  }),
  session: PropTypes.shape({
    subscribe: PropTypes.func,
    unsubscribe: PropTypes.func,
  }),
};
enricop89 commented 4 years ago

https://github.com/opentok/opentok-react/pull/108