8

I have this video, playing in zindex: -1 with a button and a text input floating over it. The issue is when the text changes, it's supposed to manipulate that state object, not fire the touchable highlight's on click function.

When I use the suggestion given yesterday, the error turns into a warning. If I type 7 random letters in the input box, I'll get 7 warnings saying: "warning bind() you are binding a component method to the component", which means the input box is continuing to call the touchable highlight's function.

I'm using this library for React Native to use it's streaming capabilities: https://github.com/oney/react-native-webrtc. It's pretty nice!

On one of it's examples, https://github.com/oney/RCTWebRTCDemo/blob/master/main.js there are these lines of code I'm fiddling with:

  _renderTextRoom() {
      return (
        <View style={styles.listViewContainer}>

          <ListView
            dataSource={this.ds.cloneWithRows(this.state.textRoomData)}
            enableEmptySections={true}
            renderRow={rowData =>
              <Text
              style={styles.whiteOut}
              >{`${rowData.user}: ${rowData.message}`}</Text>}
           />

          <TextInput
            style={[styles.whiteOut, styles.bgWhite]}
            onChangeText={value => this.setState({ textRoomValue: value })}
            value={this.state.textRoomValue}
          />

          <View style={styles.buttonContainer}>
            <TouchableHighlight
              style={styles.button}
              onPress={this._textRoomPress()}>
              <Text style={styles.bgWhite}>Send</Text>
            </TouchableHighlight>
          </View>

        </View>
      );
    },

When I enter text into the text field, the this._textRoomPress() function nested within the TouchableHighlight is firing. What!? When I comment it out, it doesn't fire.

'use strict';

import React, { Component } from 'react';
import {
  Dimensions,
  StyleSheet,
  Text,
  TouchableHighlight,
  View,
  TextInput,
  ListView,
  ScrollView
} from 'react-native';

import { userData } from '../utils/Factory';

import io from 'socket.io-client';

var socket_one = 'https://xxxxxxxxxxxxxx.herokuapp.com';

const socket = io.connect(socket_one, { transports: ['websocket'] });

import {
  RTCPeerConnection,
  RTCMediaStream,
  RTCIceCandidate,
  RTCSessionDescription,
  RTCView,
  MediaStreamTrack,
  getUserMedia,
} from 'react-native-webrtc';

const configuration = { "iceServers": [{ "url": "stun:stun.l.google.com:19302" }] };

const pcPeers = {};
let localStream;

var width = Dimensions.get('window').width; //full width
var height = Dimensions.get('window').height; //full height

function getLocalStream(isFront, callback) {

  MediaStreamTrack.getSources(sourceInfos => {
    console.log(sourceInfos);
    let videoSourceId;
    for (const i = 0; i < sourceInfos.length; i++) {
      const sourceInfo = sourceInfos[i];
      if (sourceInfo.kind == "video" && sourceInfo.facing == (isFront ? "front" : "back")) {
        videoSourceId = sourceInfo.id;
      }
    }

    getUserMedia({
      audio: true,
      video: {
        mandatory: {
          minWidth: 700, // Provide your own width, height and frame rate here
          minHeight: 700,
          minFrameRate: 30
        },
        facingMode: (isFront ? "user" : "environment"),
        optional: [{ sourceId: sourceInfos.id }]
      }
    }, function(stream) {
      console.log('dddd', stream);
      callback(stream);
    }, logError);
  });

}

function join(roomID) {
  socket.emit('join', roomID, function(socketIds) {
    console.log('join', socketIds);
    for (const i in socketIds) {
      const socketId = socketIds[i];
      createPC(socketId, true);
    }
  });
}


function createPC(socketId, isOffer) {
  const pc = new RTCPeerConnection(configuration);
  pcPeers[socketId] = pc;

  pc.onicecandidate = function(event) {
    // console.warn('onicecandidate', event.candidate);
    if (event.candidate) {
      socket.emit('exchange', { 'to': socketId, 'candidate': event.candidate });
    }
  };

  function createOffer() {
    pc.createOffer(function(desc) {
      console.log('createOffer', desc);
      pc.setLocalDescription(desc, function() {
        console.log('setLocalDescription', pc.localDescription);
        socket.emit('exchange', { 'to': socketId, 'sdp': pc.localDescription });
      }, logError);
    }, logError);
  }

  pc.onnegotiationneeded = function() {
    console.log('onnegotiationneeded');
    if (isOffer) {
      createOffer();
    }
  }

  pc.oniceconnectionstatechange = function(event) {
    console.log('oniceconnectionstatechange', event.target.iceConnectionState);
    if (event.target.iceConnectionState === 'completed') {
      setTimeout(() => {
        getStats();
      }, 1000);
    }
    if (event.target.iceConnectionState === 'connected') {
      createDataChannel();
    }
  };
  pc.onsignalingstatechange = function(event) {
    console.log('onsignalingstatechange', event.target.signalingState);
  };

  pc.onaddstream = function(event) {
    console.log('onaddstream', event.stream);
    // container.setState({ info: 'One peer join!' });
    container.setState({ info: 'Connected!' });

    const remoteList = container.state.remoteList;
    remoteList[socketId] = event.stream.toURL();
    container.setState({ remoteList: remoteList });
  };
  pc.onremovestream = function(event) {
    console.log('onremovestream', event.stream);
  };

  pc.addStream(localStream);

  function createDataChannel() {
    if (pc.textDataChannel) {
      return;
    }
    const dataChannel = pc.createDataChannel("text");

    dataChannel.onerror = function(error) {
      console.log("dataChannel.onerror", error);
    };

    dataChannel.onmessage = function(event) {
      console.log("dataChannel.onmessage:", event.data);
      container.receiveTextData({ user: socketId, message: event.data });
    };

    dataChannel.onopen = function() {
      console.log('dataChannel.onopen');
      container.setState({ textRoomConnected: true });
    };

    dataChannel.onclose = function() {
      console.log("dataChannel.onclose");
    };

    pc.textDataChannel = dataChannel;
  }
  return pc;
}

function exchange(data) {
  const fromId = data.from;
  let pc;
  if (fromId in pcPeers) {
    pc = pcPeers[fromId];
  } else {
    pc = createPC(fromId, false);
  }

  if (data.sdp) {
    console.log('exchange sdp', data);
    pc.setRemoteDescription(new RTCSessionDescription(data.sdp), function() {
      if (pc.remoteDescription.type == "offer")
        pc.createAnswer(function(desc) {
          console.log('createAnswer', desc);
          pc.setLocalDescription(desc, function() {
            console.log('setLocalDescription', pc.localDescription);
            socket.emit('exchange', { 'to': fromId, 'sdp': pc.localDescription });
          }, logError);
        }, logError);
    }, logError);
  } else {
    console.log('exchange candidate', data);
    pc.addIceCandidate(new RTCIceCandidate(data.candidate));
  }
}

function leave(socketId) {
  console.log('leave', socketId);
  const pc = pcPeers[socketId];
  const viewIndex = pc.viewIndex;
  pc.close();
  delete pcPeers[socketId];

  const remoteList = container.state.remoteList;
  delete remoteList[socketId]
  container.setState({ remoteList: remoteList });
  container.setState({ info: 'One peer leave!' });
}

socket.on('exchange', function(data) {
  exchange(data);
});
socket.on('leave', function(socketId) {
  leave(socketId);
});

socket.on('connect', function(data) {
  console.log('connected');
});

function initStream() {
  getLocalStream(true, function(stream) {
    localStream = stream;
    container.setState({ selfViewSrc: stream.toURL() });
    // container.setState({ status: 'ready', info: 'Please enter or create room ID' });
    container.setState({ status: 'connect', info: 'Connecting' });

    if (userData.inDanger) {
      join(0);
    } else {
      join(userData.userName);
      // join(userData.nowPlaying);
    }

  });

}

function logError(error) {
  console.log("logError", error);
}

function mapHash(hash, func) {
  const array = [];
  for (const key in hash) {
    const obj = hash[key];
    array.push(func(obj, key));
  }
  return array;
}

function _textRoomPress() {
  if (!container.textRoomValue) {
    return
  }
  const textRoomData = container.textRoomData.slice();
  textRoomData.push({ user: 'Me', message: container.textRoomValue });
  for (const key in pcPeers) {
    const pc = pcPeers[key];
    pc.textDataChannel.send(container.textRoomValue);
  }
  container.setState({ textRoomData, textRoomValue: '' });
}

function getStats() {
  const pc = pcPeers[Object.keys(pcPeers)[0]];
  if (pc.getRemoteStreams()[0] && pc.getRemoteStreams()[0].getAudioTracks()[0]) {
    const track = pc.getRemoteStreams()[0].getAudioTracks()[0];
    console.log('track', track);
    pc.getStats(track, function(report) {
      console.log('getStats report', report);
    }, logError);
  }
}

let container;

const Stream = React.createClass({
  getInitialState: function() {
    this.ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => true });
    return {
      info: 'Initializing',
      status: 'init',
      roomID: '',
      // isFront: true,
      isFront: false,
      selfViewSrc: null,
      remoteList: {},
      textRoomConnected: false,
      textRoomData: [],
      textRoomValue: '',
    };
  },
  componentDidMount: function() {
    container = this;
    initStream();
  },
  _press(event) {
    // this.refs.roomID.blur();
    this.setState({ status: 'connect', info: 'Connecting' });
    join(userData.userName);
    // join(this.state.roomID);
  },
  _switchVideoType() {
    const isFront = !this.state.isFront;
    this.setState({ isFront });
    getLocalStream(isFront, function(stream) {
      if (localStream) {
        for (const id in pcPeers) {
          const pc = pcPeers[id];
          pc && pc.removeStream(localStream);
        }
        localStream.release();
      }
      localStream = stream;
      container.setState({ selfViewSrc: stream.toURL() });

      for (const id in pcPeers) {
        const pc = pcPeers[id];
        pc && pc.addStream(localStream);
      }
    });
  },
  receiveTextData(data) {
    const textRoomData = this.state.textRoomData.slice();
    textRoomData.push(data);
    this.setState({ textRoomData, textRoomValue: '' });
  },
  _textRoomPress() {
    if (!this.state.textRoomValue) {
      return
    }
    const textRoomData = this.state.textRoomData.slice();
    textRoomData.push({ user: 'Me', message: this.state.textRoomValue });
    for (const key in pcPeers) {
      const pc = pcPeers[key];
      pc.textDataChannel.send(this.state.textRoomValue);
    }
    this.setState({ textRoomData, textRoomValue: '' });
  },
  _renderTextRoom() {
    return (
      <View style={styles.listViewContainer}>

              <ListView
                dataSource={this.ds.cloneWithRows(this.state.textRoomData)}
                enableEmptySections={true}
                renderRow={rowData =>
                  <Text
                  style={styles.whiteOut}
                  >{`${rowData.user}: ${rowData.message}`}</Text>}
               />

              <TextInput
                style={[styles.whiteOut, styles.bgWhite]}
                onChangeText={value => this.setState({ textRoomValue: value })}
                value={this.state.textRoomValue}
              />


              <View style={styles.buttonContainer}>
                <TouchableHighlight
                  style={styles.button}
                  onPress={this._textRoomPress()}>
                  <Text style={styles.bgWhite}>Send</Text>
                </TouchableHighlight>
              </View>

            </View>
    );
  },
  render() {
    return (
      <View style={styles.container}>
         {
          mapHash(this.state.remoteList,  (remote, index) => {

            return (
              <ScrollView key={index}>

                <RTCView key={index}  streamURL={this.state.selfViewSrc} style={styles.remoteView}>

                 <View style={styles.buttonContainer}>
                  <TouchableHighlight
                  style={styles.button}
                  onPress={this._switchVideoType}>
                  <Text>Switch camera</Text>
                  </TouchableHighlight>
                 </View>

                <View style={styles.bottomContainer}>
                  {this.state.textRoomConnected && this._renderTextRoom()}
                </View>

                </RTCView>

             )

          })
        }  
      </View>
    );
  }
});

const styles = StyleSheet.create({
  container: {
    flex: 10,
    // justifyContent: 'center',
    backgroundColor: 'rgba(0,0,0, .0)',
  },
  topContainer: {
    flex: 10,
    backgroundColor: '#c7c7c7',
  },
  bottomContainer: {
    flex: 1,
    justifyContent: 'flex-end',
    // backgroundColor: '#ffeeff',
    'zIndex': 1,
    backgroundColor: 'rgba(0,0,0, .0)',

  },
  selfView: {
    width: 0,
    height: 0
  },
  remoteView: {
    flex: 1,
    'zIndex': -1,
    // backgroundColor: '#c7c7c7',  
    backgroundColor: '#f0f0f0',
    width: width,
    height: height - 25,
    resizeMode: 'stretch', // or 'stretch'

  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  listViewContainer: {
    height: 150,
  },
  buttonContainer: {
    height: 50,
    // backgroundColor: 'powderblue',
    justifyContent: 'center',
    alignItems: 'center',
  },
  button: {
    marginTop: 50,
    marginBottom: 50,
    padding: 10,
    paddingLeft: 30,
    paddingRight: 30,
    borderWidth: 1,
    borderColor: 'rgba(0, 0, 0, .75)',
  },
  whiteOut: {
    // color: "#ffffff",
    color: "#000",
  },
  bgWhite: {
    // backgroundColor: "#ffffff"
  },
  listView: {
    // backgroundColor: "#ffffff",
    flex: 10,
    // flexDirection: 'row',
    // justifyContent: 'center',
    // alignItems: 'center',
  }
});

export default Stream;
Dr. Div
  • 951
  • 14
  • 26

3 Answers3

7

Replace it with this._textRoomPress.bind(this)

It is not firing arbitrarily, it is firing every time a render is issued. That happens because whatever you pass to an object as a prop is evaluated before being passed (as the parameters of a function are, basically), and so what you are passing is the returning value of the function, which of course is not what you want. By passing this._textRoomPress (with the optional bind in case that you want to keep the context of the object), you pass a reference to the function that will be later called by the component on the appropriate time (when the element is pressed).

martinarroyo
  • 9,389
  • 3
  • 38
  • 75
  • 1
    Remove the parenthesis, like: `this._textRoomPress.bind(this)`. The reasoning is the same, you are executing the function and applying `bind` to the return value. Your function is likely not returning anything, so you get `undefined`, hence the error. – martinarroyo Dec 12 '16 at 18:04
  • last question, why am I getting this error. I'm sure it's a binding issue. undefined is not an object evaluating('pc.textDataChannel.send'). Sometimes it works and sends the data to the server but most of the time it doesn't. Thanks in advance @martinarroyo – Dr. Div Dec 12 '16 at 18:33
  • @clxxxii It might or it might not be. Binding issues affect only values that you access via the `this` variable. If that is the case, check that you are binding things correctly or post the code so that we can have a look at it. The other possibility is that either `pc` or `textDataChannel` are `undefined`. `undefined` values have no properties, and therefore you cannot access them. Check the places where you are updating those two (since you say that sometimes it works, it is likely that you have something changing its value). – martinarroyo Dec 12 '16 at 19:33
  • Warning: bind(): You are binding a component method to the component. React does this for you automatically in a high-performance way, so you can safely remove this call. This fires every time i type something in the input box. The function is continuing to fire although it's attached to the touchable highlight. I have two problem. I'll the class above. – Dr. Div Dec 12 '16 at 22:17
  • In the updated code you still have `this._textRoomPress()` instead of `this._textRoomPress`. The function continues to fire because of this. You don't need to `bind` in this particular case since you are not accessing the `this` reference. – martinarroyo Dec 14 '16 at 14:28
  • I took the () off. and the error is still occurring. The highlight's function is continuing to fire although I'm entering text into the input. I appreciate your help man. – Dr. Div Dec 14 '16 at 18:00
  • I am not really sure what is happening then... Do you have your entire project in some public repo, so I can try and replicate the full thing on my computer? – martinarroyo Dec 14 '16 at 18:18
  • i would rather not share it publicly - how do i share my emails or twitter. i'm at-HevyRailLazer or download this - cause I haven't changed too much https://github.com/oney/RCTWebRTCDemo – Dr. Div Dec 14 '16 at 18:41
3

Since you're using createClass and not the es6 syntax, all the methods are already autobinding to the Component. Simply just change your onPress to:

onPress={this._textRoomPress}>

If you use onPress={this._textRoomPress()}> It is instantly invoking that function anytime your component gets rendered.

Matt Aft
  • 8,742
  • 3
  • 24
  • 37
2

In javascript you use <function name>() to invoke a function... What you are doing here is simply invoking that function every time that _renderTextRoom()gets called rather than assigning it to the onPress prop. What I would suggest is that you pass an anonymous function in as the prop (without calling it) which than returns the invocation of this._textRoomPress. ES6 arrow functions make this super easy because they do not bind their own this more info here

  <View style={styles.buttonContainer}>
    <TouchableHighlight
      style={styles.button}
      onPress={() => this._textRoomPress()}>
      <Text style={styles.bgWhite}>Send</Text>
    </TouchableHighlight>
  </View>
Maxwelll
  • 2,174
  • 1
  • 17
  • 22