opentok / opentok-react-native

OpenTok React Native - a library for OpenTok iOS and Android SDKs
https://tokbox.com/
MIT License
211 stars 156 forks source link

Switching user from publisher to subscriber #206

Closed snobear closed 5 years ago

snobear commented 5 years ago

Hi, I'm building a broadcast one-to-many style app. At any given time, a user can become a "performer" aka publisher in Opentok terms, and everyone else a subscriber. There is a queue of performers, so once a performer is done, they become a subscriber and the next person in the queue becomes the sole performer.

The issue I'm having is that when a user goes from publisher to subscriber, there is a bunch of feedback from the microphone because they are still "seen" as a publisher. I assumed at first that with the following code, the OTPublisher would be unmounted and OTSubscriber mounted in its place:

        <OTSession
          apiKey={this.apiKey}
          sessionId={this.props.sessionId}
          token={this.props.opentok.token}
          eventHandlers={this.sessionEventHandlers}>
          { this.state.isPerformer ? <OTPublisher /> : <OTSubscriber /> }
        </OTSession>

Per the docs, this is not the case:

The OTPublisher component will initialize a publisher and publish to the specified session upon mounting. To destroy the publisher, unmount the OTPublisher component. Please keep in mind that the publisher view is not removed unless you specifically unmount the OTPublisher component.

So that makes sense as to the issue I'm seeing. So my question is, how can I "switch" the user from publisher to subscriber? Is there a way to unpublish a user first?

The only thing I could think of was to have some setState that first hides then shows OTSession, but that could get ugly. Any ideas? Ideally, the switch should be as seamless as possible.

snobear commented 5 years ago

From what I understand, the above code should unmount OTPublisher and mount OTSubscriber if you were to toggle this.state.isPerformer from true to false. But after the toggle, the publisher-turned-subscriber still seems to be publishing. Or at least the mic has stayed on and is causing audio feedback.

Let me know if you have any thoughts on this. I will see if I can provide a sample git repo on the issue so you can reproduce.

snobear commented 5 years ago

@msach22 in general, if I wanted to switch from a publisher to a subscriber in the same session, would I have to totally disconnect the current session as a publisher (with a publisher token) and reconnect as a subscriber (with a subscriber token)?

msach22 commented 5 years ago

@snobear Apologies for the delay, you can pass in publisher event handlers to the OTPublisher component to know when the publisher stream has been destroyed:

    this.pubEvents = {
      streamCreated: event => {
        console.log('pub streamCreated', event);
      },
      streamDestroyed: event => {
        console.log('pub streamDestroyed', event);
      }
    }

You can then mount the subscriber component when you know the publisher stream has been destroyed. There was a bug in the library where the streamDestroyed event was not firing for the publisher, but this has been fixed as of v0.9.4. Please use the latest library to check out the fix.

msach22 commented 5 years ago

You don't have to disconnect the client because then you would have to reconnect them to make them a subscriber. I recommend trying out the approach above - the unmounting of the publisher should unpublish the publisher.

if that doesn't work out, I'd love to get on a call with you and think of a better solution then having to disconnect and reconnect.

snobear commented 5 years ago

Thanks for the reply. I’ve actually tried switching pub/sub type while still connected and by attempting to terminate the entire OTSession first then reconnecting as the new type, but neither works.

I added some logging statements in src/OTPublisher.js and can confirm the componentWillUnmount is properly called and the publisher destroy routine is called without error. But when attempting to reconnect as a subscriber the stream never comes though. I’m still digging into it to get a better idea.

Will try to reproduce this in a simpler example using a clone of the basic video chat sample late next week, and then maybe we can hop on a call to discuss.

msach22 commented 5 years ago

@snobear Thanks for the update. Are you using the playground tool as the remote person?

snobear commented 5 years ago

No I haven’t tried the playground tool, thanks for pointing me to that. I’ve been testing with a directly connected device and the iOS simulator in tandem.

msach22 commented 5 years ago

Happy to share - the playground tool will make it easier to test because you can test all sorts of features (archiving, broadcasting, etc)

snobear commented 5 years ago

Hi @msach22,

I was able to reproduce what I'm seeing with a simple example, as seen below. Perhaps my understanding or expectations of OT are incorrect, so please give it a shot and we can hop on a call if you'd like to go through it.

Add your settings to the CHANGE ME section. I specifically generated and used subscriber and publisher type tokens for this.subscriberToken and this.publisherToken, respectively. I tested only on iOS in the simulator and Opentok playground tool for this example, but I saw the same on physical iOS devices when working in my own project.

# App.js
import React, { Component } from 'react';
import { View, Button } from 'react-native';
import { OTSession, OTPublisher, OTSubscriber } from 'opentok-react-native';

export default class App extends Component {
  constructor(props) {
    super(props);

    //**** CHANGE ME
    this.apiKey = '';
    this.sessionId = '';
    this.subscriberToken = '';
    this.publisherToken = '';
    //****

    this.state = {
      isPublisher: false,
      token: this.subscriberToken
    };

    // Opentok handlers
    this.sessionEventHandlers = {
      error: error => {
        console.log('Opentok session error',error);
      },
      sessionConnected: () => {
        console.log('Opentok session connected');
      },
      sessionReconnected: () => {
        console.log('Opentok session sessionReconnected');
      },
      sessionReconnecting: () => {
        console.log('Opentok session sessionReconnecting...');
      },
      sessionDisconnected: () => {
        console.log('Opentok session disconnected');
      },
      streamCreated: event => {
        console.log('Opentok session stream created',event);
      },
      streamDestroyed: event => {
        console.log('Opentok session stream streamDestroyed',event);
      },
      connectionCreated: event => {
        console.log('Opentok session connectionCreated',event);
      },
      connectionDestroyed: event => {
        console.log('Opentok session connectionCreated',event);
      },
      connectionCreated: () => {
        console.log('Opentok session connectionCreated');
      },
      connectionCreated: () => {
        console.log('Opentok session connectionCreated');
      },
      connectionCreated: () => {
        console.log('Opentok session connectionCreated');
      },
      signal: event => {
        console.log('Opentok session signal',event);
      },
    };

    this.subscriberEventHandlers = {
      error: (error) => {
        console.log('Subscriber error',error);
      }
    };
    this.publisherEventHandlers = {
      error: (error) => {
        console.log('Publisher error',error);
      },
      streamCreated: event => {
        console.log('Publisher stream created!', event);
      },
      streamDestroyed: event => {
        console.log('Publisher stream destroyed!', event);
      }
    };

  }

  togglePubSub = () => {
    let _token = null;

    if (this.state.isPublisher) {
      _token = this.subscriberToken;
    } else {
      _token = this.publisherToken;
    }

    this.setState({
      isPublisher: !this.state.isPublisher,
      token: _token
    });
  }

  render() {
    if (this.state.isPublisher) {
      console.log('I am currently a Publisher');
    } else {
      console.log('I am currently a Subscriber');
    }

    return (
      <View style={{ flex: 1, flexDirection: 'row' }}>
        <OTSession
         apiKey={this.apiKey}
         sessionId={this.sessionId}
         token={this.state.token}
         eventHandlers={this.sessionEventHandlers}>
          { this.state.isPublisher
            ? <OTPublisher style={{ width: 300, height: 300 }} eventHandlers={this.publisherEventHandlers} />
            : <OTSubscriber style={{ width: 300, height: 300 }} eventHandlers={this.subscriberEventHandlers} />
          }
        </OTSession>
        <View style={{ position: 'absolute', bottom: 100, left:100, borderWidth: 1, borderColor: '#000' }}>
          <Button
            onPress={this.togglePubSub}
            title={ this.state.isPublisher ? 'Switch to subscriber' : 'Switch to publisher' }
            color="#000"
          />
        </View>
      </View>
    );
  }
}

Steps

Before you start, join the session in the playground tool in your browser and publish your stream to help troubleshoot. We'll refer to this as "the playground user".

Attempt 1: Start as a subscriber

  1. Fire up the example RN app with isPublisher: false and use the subscriberToken in the constructor, and it should start as a subscriber with only OTSubscriber being mounted. You should see the playground user's stream.
  2. Click the Switch to publisher button...and here is where it breaks down.

Expected Results when clicking the switch button:

Actual results:

Attempt 2: Start as a publisher

  1. Fire up the example RN app with isPublisher: true and use the publisherToken in the constructor, and it should start as a publisher with only OTPublisher being mounted. You should see your stream (teapot).
  2. In the playground tool, the playground user AND your stream (teapot) appear successfully.
  3. Click the Switch to subscriber button...and here is where it breaks down...

Expected Results when clicking the switch button:

Actual results:

TLDR; try the above App.js and see if it works as you'd expect :). @msach22 lets hop on a call and discuss further if you need help understanding the above book.

snobear commented 5 years ago

OK, I added some event handlers and just did some testing without any additional users, i.e. not using playground. When initializing the above example app as a subscriber, then switching to publisher, I get the following OTPublisher error:

code: 1500
message: "Unable to Publish."

According to the docs, error code 1500 is:

Unable to Publish. The client's token does not have the role set to publish or moderator. Once the client has connected to the session, the capabilities property of the Session object lists the client's capabilities.

Even though the App.js component rerenders (after this.state.token and this.state.isPublisher is updated to publisher settings), it doesn't appear that OTSession is ever updated to use the new token.

snobear commented 5 years ago

I just tested having all users initialize the app component with a publisher token, and this has the switching working successfully 👍. Any downside to using a publisher token for everyone? In our application, anyone can become a publisher at any time, so I guess this is OK. I just figured having a user be a subscriber when they are not publishing anything is a safer method so we're not accidentally publishing audio/video for them.

Pretty sure I had tested using pub tokens for all users in our app, but was still hitting the issue of a subscriber's audio feeding back because the mic was still on. I'll do some debugging and hopefully figure out the (dumb) mistake. Keep you posted.

msach22 commented 5 years ago

@snobear Thanks for the detailed sample app, reproduction steps, and what you tried. One key thing to note is that when you change the token from publisher to subscriber token, the library is not creating a new session with the updated token. Think of it as an instance that cannot change, the only way to change the token is to unmount the OTSession component and mount it again with the new token.

Using the publisher token does not have any implications even if they are not publishing. The tokens are just used for define "permissions". In this case, the users will be able to publish and subscribe only if the code allows it. The token itself doesn't have the ability to do that.

snobear commented 5 years ago

Closing, as using the publisher token for everyone solved my main issue.

Re: the mic feedback, I have only had it happen a few times, so if it happens again in a way that I can reproduce I'll file a new issue. Thanks @msach22 !

msach22 commented 5 years ago

It was great chatting with you @snobear!

snobear commented 5 years ago

@msach22 great talking with you as well! Glad we hopped on a call, it was extremely helpful!

andikare commented 5 years ago

i am sorry, i have a question for that, i want to make app in android with react native 1 to many like a bigo app, example : publisher can make 1 room to start live streaming and other user in other android can click and join to that room, the question is how we make the room with session id opentok ? or you have any idea for this ? thank you, sorry for my bad english i hope you understand what i mean

snobear commented 5 years ago

@andikare the sample App.js I posted above ^ should get you started.

To get an API key you need to sign up on tokbox.com. Then you can create sessions and tokens from the toxbox.com account page for testing.

In my app, I have a session ID for each "room". Users get new tokens whenever they join a room. As mentioned, you can generate session IDs and tokens via tokbox.com, but you'll eventually need to roll your own server-side code to generate these for your project. See these docs:

https://tokbox.com/developer/guides/create-session/ https://tokbox.com/developer/guides/create-token/

We use Firebase Cloud Functions (nodejs) for that, but you can use any of the language SDKs mentioned in the docs.

Hope that helps.

andikare commented 5 years ago

@snobear Thank you sir, i was create and make session with node.js, i have question again, Do subcriber tokens and publisher tokens have to be changed every time they enter the room or not? and in https://tokbox.com/developer/guides/create-session/ it is token for subcriber or token publisher ? because just show 1 token in there,

thank you

snobear commented 5 years ago

You will want to reuse the session ID that you create for the room. For our application, we store the session ID along with the room data in our database. Users must connect to the same session ID, so its important that all users use and reference the same session ID. There is no relation for subscriber vs publisher when you are talking about session ID.

However, for subscriber and publisher tokens, Opentok recommends that you issue a new token every time a user connects to the room/session:

Tokens are cheap to generate. They are generated just with a hashing function and your secret. There is no API call to our servers used when generating a token. We recommend:

  • Generating a new token for every user at the time they try to connect. Tokens have an expiration time, which by default is 24 hours after the token is created. After the expiration time, you cannot use the token to connect to the session.
  • Not storing tokens or trying to reuse them.
  • Using connection data to identify users. Connection data is a secure way to store information about your users (such as a user ID, which can help you identify your users in your application).
andikare commented 5 years ago

thankyou sirr @snobear i have session id with nodejs and look like this { "apiKey": "**", "sessionId": "2_MX40NjI1Mzk3Mn5-MTU0OTQ0**", "token": "T1==cGFydG5lcl9pZD00NjI1Mzk3MiZzaWc9YjMwMjIxNTAzNjNkNzBkMTlhMDA1YjNhODhhMDA3NTQzNDQwMmI4ZTpzZXNzaW9uX2lkPTJfTVg0ME5qSTFNemszTW41LU1UVTBPVFEwTnpJek1UUTBOWDVVVVhrM1VtZzRTVlpTZEhObU9VUnVlVUZUUTA1RGIyaC1mZyZjcmVhdGVfdGltZT0xNTQ5NDQ3MjMyJm5vbmNlPTAuODk3Nzc4NjM1Mjg4MzU0OCZyb2xlPXB1Ymxpc2hlciZleHBpcmVfdGltZT0xNTQ5NTMzNjMyJmluaXRpYWxfbGF5b3V0X2NsYXNzX2xpc3Q9" }

The text json above shows a token, question is what token it is ? its publisher token or subcriber token ? thank you for your time

snobear commented 5 years ago

Hi @andikare, I have no idea just looking at it. But don’t waste your time trying to figure it out; instead, you should just generate new tokens. You will specify publisher or subscriber when generating the token, so the token type is whatever you’ve requested. I believe “subscriber” is the default if you don’t explicitly specify the type. I would recommend reading up on the docs linked above first. Also, please open a new Github issue so we can keep this one specific to the original question. Thanks 🙂

andikare commented 5 years ago

@snobear thank you sirr, i was trying to make that token to subcriber and publisher and still work , thank you for your answer , if i have the question again can i email you ?

thank you again, you are so kind 👍