Help getting started with secure IP Cam streaming

Julien

Member
Hello everybody, I am just getting started with WCS5 and my goal is to port an existing application that uses a different media server that we have grown very unsatisfied with to WCS5 to see if it performs better.

The current application allows users to connect to several IP cams, it is split into 3 separate parts: the client (html5 website), the media server that serves the RTSP stream of the IP cams to the clients with WebRTC and a Node.js app in the middle that handles the logic of checking if the users are allowed to see those streams, handle all the handshakes and DB logic, etc.
The way it works is the client sends a request to the node app via websockets asking to view a specific IP cam, then the node app talks to the media server handling all the SDP offer/answer logic, etc and sends back to the client the stream of the IP cam.

Now going through the examples and docs of WCS5, using the "Player" demo i can successfully connect to one of those IP cams and i get the stream properly. So far so good.
But as far as i can tell, the flashphoner client communicates directly with the media server, meaning the full URL of WSC5 as well as of the IP cam is visible (including its username/password) to the users and I can't do extra processing. I've been searching through this forum and found this link that seems to address (partly) this subject. From what I understand from that thread, one way to solve this would be to make an EchoApp that responds to certain REST calls and do my custom processing in there, is that correct? Or can i use the approach of my current system and simply have a Node.js app that communicates directly with WCS5 to create/stop the streaming of the IP Cam and simply sends back to the client (browser) the stream?

I hope my question is not too confusing, any help appreciated!
 

Max

Administrator
Staff member
Hello

Thanks for detail description. Your question is enough clear.
From your description, I see you want to secure your application and hide RTSP urls from end-users.
And you want authenticate users permitting or rejecting playback attempts.
With WCS it can be done using REST-methods (hooks)
Docs: https://flashphoner.com/docs/wcs5/wcs_docs/html/en/wcs-rest-methods
Using REST-methods, you can
1. Authenticate playback attempt or reject it.
2. Dynamically map RTSP urls to alias names, for example: rtsp://host:554/live.sdp to 'stream1'.

Example of authentication:
1. Browser's JS sends command session.createStream({name:'stream1',custom:{token:'12345'}}).play();
2. WCS receives 'play' command over websockets.
3. WCS sends REST/JSON request to Node.js.
The request contains token:'12345' parameter.
Example:
https://flashphoner.com/docs/wcs5/w...s/index.html?direct_invoke_authentication.htm
4. You can authenticate by token on Node.js and reply either with HTTP 200 OK (authenticated), or HTTP 403 Forbidden (authentication failed).
5. WCS will return authentication result to browser.

Here you have to generate a token on user's page, that will be used for authentication on your Node.js server.

Example of RTSP URL mapping:
1. Browser's JS sends command session.createStream({name:'stream1',custom:{token:'12345'}}).play();
2. WCS receives 'play' command over websockets.
3. WCS sends REST/JSON request to Node.js.
The request contains filed:
Code:
name:'stream1'
4. Node.js sends HTTP 200 OK reply with body
Code:
{
name:'rtsp://host:554/live.sdp'
}
5. WCS pools RTSP stream and the stream URL is hidden from end-user.

Here you overwrite stream name from 'stream1' to 'rtsp:...' on your Node.js server.
The OVERWRITE policy should be configured as described in the docs:
https://flashphoner.com/docs/wcs5/w...od_connect_nonfiguring_other_rest_methods.htm

Therefore you have to implement two REST hooks (HTTP JSON urls):
Code:
/connect
/play
These two urls will serve authentication and stream name security.
 

Julien

Member
Hello,
sorry for the delay in my answer, i had to work on something different in the meantime. Now I am ready to get back to this :)
Thank you for your detailed answer, it helps a lot, but I do have a couple of questions:
- how do i tell WCS the address of my nodejs app (where it should send the REST requests)? I couldnt really find this in the docs, but i get lost there easily :D Is this done in a config file or...?
- it seems to me that using this approach the url of WCS is visible to the users, is there any way to hide/avoid that?

Thanks!
 

Max

Administrator
Staff member
Hi,
how do i tell WCS the address of my nodejs app (where it should send the REST requests)?
On server
ssh -p 2000 admin@localhost
>
password admin
>show apps
Code:
admin defaultApp        defaultApp        1  http://localhost:9091/EchoApp
You can replace defaultApp with your REST application or create new app with your own REST URL.
More info can be found from docs: https://flashphoner.com/docs/wcs5/w...and_line_interface-application_management.htm
it seems to me that using this approach the url of WCS is visible to the users, is there any way to hide/avoid that?
What URL do you mean?
 

Julien

Member
Hi,
thank you for your answer, i have things working now, but i have a few questions:

- Let's say i have 2 different IP cams and the user can view the stream of both of them. Do I have to create/destroy the session every time the user switches between those two cams (while staying on the same page)? Or can I reuse the session and just call createStream on that session passing it the name of the cam as a custom parameter every time the user switches camera (and then overwrite the name parameter in the REST hook)?

- How can i check if the user is watching two streams at the same time (using separate windows or tabs)?

- I've seen in the docs that i can specify the ice servers dynamically when creating the session. Is there anything i have to configure on the server side? If yes how can it be done dynamically (i use Twilio's TURN server and the credentials have to be requested every 24h so it cannot be hardcoded in config files).

What URL do you mean?
The url of the WCS5 server that is passed when creating the session. Is there anyway to hide it or encrypt it?

Thank you
 

Max

Administrator
Staff member
Hello
Let's say i have 2 different IP cams and the user can view the stream of both of them. Do I have to create/destroy the session every time the user switches between those two cams (while staying on the same page)?
No you don't need to disconnect.
You just do stream1.play() / stream1.stop(), and stream2.play() / styream2.stop() for your cams.
Take a look at our demo sample for two players:
https://wcs5-eu.flashphoner.com/demo2/2players
https://wcs5-eu.flashphoner.com/client2/examples/demo/streaming/2players/2players.html
https://wcs5-eu.flashphoner.com/client2/examples/demo/streaming/2players/2players.js
As you can see, you can display two or more players on the same web page and you can manage these players.
Or can I reuse the session and just call createStream on that session passing it the name of the cam as a custom parameter every time the user switches camera (and then overwrite the name parameter in the REST hook)?
Yes, if you stopped stream and it is in STOPPED state, you have to make session.createStream(), to create a new stream and play.
and then overwrite the name parameter in the REST hook
Yes you can overwrite the stream name using REST hooks.
How can i check if the user is watching two streams at the same time (using separate windows or tabs)?
You can add authentication / identification information which will identify user connection.
Example:
Add to session:
Code:
Flashphoner.createSession({custom:{username:'alice'}});
Add to play()
Code:
session.createStream({custom:{username:'alice'}, name:'stream1'}).play();
If you add object custom:{}, this object will be passed to REST Hook request.
So you will receive information about connected user and you will able to see if the same user play two cams on the same page.
The url of the WCS5 server that is passed when creating the session. Is there anyway to hide it or encrypt it?
It looks meaningless, because any developer can open developer console and see address of server that is used for websockets (wss://host:8443).
You can't hide this as you can't hide address of HTTP server where you downloaded a web page.
No. Encryption of URL is not supported.
 

Julien

Member
Hi, I am now in the process of integrating all the work done into the existing application and i am running into some problems:

- when the stream is supposed to start playing, it doesn't play and looking at the console i see thousands of the following deprecation message while the page becomes unresponsive :
URL.createObjectURL(stream) is deprecated, please use elem.srcObject = stream instead.
Then the following error is appears:
Uncaught RangeError: Maximum call stack size exceeded
(the file originating the warnings and error is Flashphoner.min.js)

- I noticed that after webcallserver is running for a while i am unable to connect to an IP cam, even using the demo, the session displays ESTABLISHED and then FAILED. Once i restart webcallserver it works again for a while and then the same problem again. Any idea why this happens? How can i debug this problem?

thank you!
 

Max

Administrator
Staff member
Hi
Do you have the same issue with flashphoner.js file? Not with flashphoner.min.js.
Please send SSH access to logs@flashphoner.com and dashboard admin password, we will check.
- I noticed that after webcallserver is running for a while i am unable to connect to an IP cam, even using the demo, the session displays ESTABLISHED and then FAILED. Once i restart webcallserver it works again for a while and then the same problem again. Any idea why this happens? How can i debug this problem?
Try to add more memory to the server. I.e. 2048M or 4096M.
 

Julien

Member
Hi,
Found the source of the problem that was causing the errors, it was because my application was including adapter.js for some other stuff and it seems Flashphoner.js also includes it so both of them conflicted, once i removed adapter.js it started working fine.

Is there any example on how to use sendData?
 

Max

Administrator
Staff member

Julien

Member
For now I need to use it to send an object to the REST back-end from the browser, which i see works fine using session.sendData and receiving it using the REST method OnDataEvent.
I would also need to send some object from the REST back-end to the browser, I would need to do it in two different ways:
  • As a response of an OnDataEvent
  • As a stand-alone event, for example after a long database operation as been completed.
Is this possible?

Also could you explain me what these fields in the body of every REST method are?
  • nodeId: I'm guessing this one is simply a unique ID for the WCS and the public IP from where it is running)
  • operationID: I'm guessing this is again a unique ID generated for each operation (REST method) executed
  • sessionId: This one is the one i am most interested in, this seems to be string generated from the combination of the user's public IP and port used and the WCS private IP and port used. Am right in assuming that this never changes between operations and that i can rely on this to know if it's the same user every time?
Thanks
 

Max

Administrator
Staff member
Hello

As a response of an OnDataEvent
Let me check this. I will report if this is possible.
As a stand-alone event, for example after a long database operation as been completed.
Yes you can do that using direct API method
Code:
https://host:8888/rest-api/data/send
{
..
}
See REST API docs - method /data/send:
https://flashphoner.com/docs/wcs5/wcs_docs/html/en/wcs-rest-api/index.html?api_methods.htm

As you can see we have two REST APIs
1) REST hooks
2) Direct REST API
You can use both hook and direct API to implement your case.

nodeId: I'm guessing this one is simply a unique ID for the WCS and the public IP from where it is running)
Yes.
operationID: I'm guessing this is again a unique ID generated for each operation (REST method) executed
Yes.
sessionId: This one is the one i am most interested in, this seems to be string generated from the combination of the user's public IP and port used and the WCS private IP and port used.
Yes it says that user has ip : port and you can use this as an unique identifier.
Am right in assuming that this never changes between operations and that i can rely on this to know if it's the same user every time?
Yes you are right. The session ID is assigned during first connection handshake and never changed within the user sessions.
However to ensure 100% uniqueness, you may consider three following fields:
Example
Code:
sessionId" : "/195.11.25.11:54249/46.101.108.90:8443",
"clientOSVersion" : "5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"clientBrowserVersion" : "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36"
There is zero possibility that two users have the same port and browser / system details. So you can get a hash from these 3 fields to get an unique identifier.

And finally you can track connected users using custom fields. For example you can pass a token or PHP session ID or whatever you want via the custom object.
Example:
Code:
Flashphoner.createSession({urlServer:'wss://host:8443',custom:{token:'123'}});
 

Max

Administrator
Staff member
Please ignore information about clientOSVersion and clientBrowserVersion.
It can be used for initial authentication, however middle REST requests do not send such data. It just sends sessionId.
 

Max

Administrator
Staff member
As a response of an OnDataEvent
No, currently you can't get a custom response if you do session.sendData

Here you can see example:
Code:
session.sendData({}).then(function(info){},function(info){});
The first param is function called on accepted event
The second param is function called on failed event

ACCEPTED - means your request was accepted by the REST end
FAILED - means something was wrong and request was not accepted by backed REST
 

Julien

Member
I got it working with the Direct REST API (/rest-api/data/send), thanks!

Now i need to be able to stop at any moment from the backend a client from viewing an IP cam stream that he/she is already viewing. Looking at the methods available from that direct API it looks to me that it would be either /rtsp/terminate or /stream/terminate, which one is it?
if it's /rtsp/terminate, what is the uri parameter and where do i find it?
And if its the /stream/terminate, what is the mediaSessionId parameter and where do i find it?
I would assume i could also simply terminate the session of the client using /connection/terminate since i know the the client's sessionId but i would rather simply stop the video and leave the client's session active.

thanks
 

Max

Administrator
Staff member
looks to me that it would be either /rtsp/terminate or /stream/terminate, which one is it?
If you do /rtsp/terminate, you close connection between WCS server and IP cam. So all viewers (browsers) will stop playing the stream.
If you do /stream/terminate, you can close particular viewer playback session.
if it's /rtsp/terminate, what is the uri parameter and where do i find it?
The uri parameter is RTSP URL.
For example: rtsp://host:554/stream.sdp
And if its the /stream/terminate, what is the mediaSessionId parameter and where do i find it?
The mediaSessionId is passed to REST in the StreamStatusEvent.
For example below you can see that mediaSessionId is 84ba8380-b54b-11e7-b231-1daffefe52a3
It is unique identifier of the playback stream.
Example:
Code:
URL:http://localhost:9091/EchoApp/StreamStatusEvent
OBJECT:
{
  "nodeId" : "ah20GKYnbYKxjJmdh1zDs6cllWkn0Q0t@46.101.108.90",
  "appKey" : "defaultApp",
  "sessionId" : "/95.21.20.25:65164/46.101.108.90:8443",
  "mediaSessionId" : "84ba8380-b54b-11e7-b231-1daffefe52a3",
  "name" : "rtsp://str81.creacast.com/grandlilletv/low",
  "published" : false,
  "hasVideo" : true,
  "hasAudio" : true,
  "status" : "PLAYING",
  "audioCodec" : "opus",
  "videoCodec" : "H264",
  "record" : false,
  "width" : 0,
  "height" : 0,
  "bitrate" : 0,
  "quality" : 0,
  "createDate" : 1508472143840,
  "mediaProvider" : "WebRTC",
  "history" : false,
  "origin" : "https://wcs5-eu.flashphoner.com"
}

URL:http://localhost:9091/EchoApp/StreamStatusEvent
OBJECT:
{
  "nodeId" : "ah20GKYnbYKxjJmdh1zDs6cllWkn0Q0t@46.101.108.90",
  "appKey" : "defaultApp",
  "sessionId" : "/95.21.20.25:65164/46.101.108.90:8443",
  "mediaSessionId" : "84ba8380-b54b-11e7-b231-1daffefe52a3",
  "name" : "rtsp://str81.creacast.com/grandlilletv/low",
  "published" : false,
  "hasVideo" : true,
  "hasAudio" : true,
  "status" : "PLAYING",
  "audioCodec" : "opus",
  "videoCodec" : "H264",
  "record" : false,
  "width" : 0,
  "height" : 0,
  "bitrate" : 0,
  "quality" : 0,
  "createDate" : 1508472143840,
  "mediaProvider" : "WebRTC",
  "history" : false
}
 

Julien

Member
Hi,
so i got everything working but i noticed a weird behavior, the STREAM_STATUS.PLAYING event always fires 2 times on the client side (browser), this happens also with the stream player demo so i know it's not from my code, why is this happening?

Now that i am testing the app on the production server i am having a problem with SSL, I've followed the steps from here adapted to my files:
Code:
> sudo keytool -delete -alias selfsigned -keystore /usr/local/FlashphonerWebCallServer/conf/wss.jks

> openssl pkcs12 -export -out my.example.com.p12 -inkey /etc/nginx/ssl/example.key -in /etc/nginx/ssl/example.crt -certfile /etc/nginx/ssl/example.chained.crt -name my.example.com

> sudo keytool -importkeystore -srckeystore my.example.com.p12 -srcstoretype PKCS12 -destkeystore /usr/local/FlashphonerWebCallServer/conf/wss.jks
Entry for alias my.example.com successfully imported.
Import command completed:  1 entries successfully imported, 0 entries failed or cancelled

> sudo service webcallserver stop
> sudo service webcallserver start

> sudo netstat -nlp | grep java
tcp        0      0 0.0.0.0:843             0.0.0.0:*               LISTEN      1823/java     
tcp        0      0 0.0.0.0:1099            0.0.0.0:*               LISTEN      1270/java     
tcp        0      0 0.0.0.0:1935            0.0.0.0:*               LISTEN      1823/java     
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      1823/java     
tcp        0      0 172.29.40.131:30000     0.0.0.0:*               LISTEN      1823/java     
tcp        0      0 0.0.0.0:2000            0.0.0.0:*               LISTEN      1270/java     
tcp        0      0 0.0.0.0:8081            0.0.0.0:*               LISTEN      1823/java     
tcp        0      0 0.0.0.0:8082            0.0.0.0:*               LISTEN      1823/java     
tcp        0      0 0.0.0.0:50999           0.0.0.0:*               LISTEN      1823/java     
tcp        0      0 0.0.0.0:8443            0.0.0.0:*               LISTEN      1823/java     
tcp        0      0 0.0.0.0:8444            0.0.0.0:*               LISTEN      1823/java     
tcp        0      0 0.0.0.0:8445            0.0.0.0:*               LISTEN      1823/java     
tcp        0      0 0.0.0.0:9091            0.0.0.0:*               LISTEN      1270/java     
tcp        0      0 0.0.0.0:47878           0.0.0.0:*               LISTEN      1823/java     
tcp        0      0 0.0.0.0:554             0.0.0.0:*               LISTEN      1823/java     
tcp        0      0 0.0.0.0:1098            0.0.0.0:*               LISTEN      1823/java     
udp        0      0 172.29.40.131:30000     0.0.0.0:*                           1823/java     
udp        0      0 0.0.0.0:1935            0.0.0.0:*                           1823/java
everything looks fine but then when i try to connect i get the following:
Code:
WebSocket connection to 'wss://my.example.com:8443/' failed: Error in connection establishment: net::ERR_TIMED_OUT
What am I doing wrong?

PS: The certificate was emitted by GoDaddy and it is a wildcard (*.example.com), just in case this information matters.
 

Max

Administrator
Staff member
Hello

Regarding two PLAYING events. We will check.

Make sure port 8443 is open on your firewall
You can check by this command from your PC
Code:
telnet host 8443
You can also list certificates to be sure it is imported properly:
Code:
keytool --list -keystore /usr/local/FlashphonerWebCallServer/conf/wss.jks
If it does not help, please send us logs to logs@flashphoner.com, we will check.
  • WCS_HOME/logs/server_logs/flashphoner.log
  • WCS_HOME/conf
You can also send SSH access. Then we will able to check server directly over ssh.
 

Julien

Member
Hi,
There is no firewall, telnet seems to connect fine:
Code:
> telnet my.example.com 8443
Trying xx.xx.xx.xx...
Connected to my.example.com.
Escape character is '^]'.
The certificate seems to be properly imported also:
Code:
> keytool --list -keystore /usr/local/FlashphonerWebCallServer/conf/wss.jks
Enter keystore password:  

Keystore type: JKS
Keystore provider: SUN

Your keystore contains 1 entry

my.example.com, Oct 23, 2017, PrivateKeyEntry, 
Certificate fingerprint (SHA1): xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
All the configurations of the WCS are the default ones, i didn't change anything, i will now send an email to logs@flashphoner.com with the SSH access so that you can have a look but at first glance looking in the logs i see the following a lot:
Code:
11:41:22,465 WARN       WSServerHandler - WS-pool-7-thread-1 Close channel [id: 0x4348f89e, /xx.xx.xx.xx:34329 => /xx.xx.xx.xx:8080] because: org.jboss.netty.handler.codec.http.websocketx.WebSocketHandshakeException 'not a WebSocket handshake request: missing upgrade'
thanks!
 

Max

Administrator
Staff member
Could you check in Chrome browser
https://my.example.com:8888
port 8888 and port 8443 use the same wss.jks keystore
So please check this in Chrome / Developer tools / Security. What does it display?
To speed up issue resolution, please move WCS server to a public virtual server where we can connect over SSH or test it from browser.
We still need more logs / configs to localize the issue.
 
Top