A streamer example with publishing/playback automatic restore
This example shows how to restore stream publishing/playback automatically:
- changing publishing codec to VP8 when H264 publishing fails
- republish a stream if browser stops sending media packets (this is detected by video bitrate drop to 0)
- when network is changing (form Wi-Fi to LTE and vice versa)
- when streaming fails due to server connection breaking (including server restart) or due to stream publishing stopping by other side
Bitrate checking parameters
- Check bitrate - check if bitrate drops to 0
- Change codec - change H264 codec to VP8 if bitrate drop is detected
- Bitrate check interval - publishing bitrate checking interval
- Max tries - maximium number of subsequent bitrate drops to 0
Connection restoring parameters
- Restore connection - restore connection if session breaks or publishing/playback is failed
- Timeout - connection restore tries interval
- Max tries - maximum number of connection restore tries
- Missing pings - maximum number of subsequent missing pings from server (0 disables ping checking)
- Pings check period - pings checking interval (0 disables ping checking)
Code of the example
The example code is available on WCS server by the following path:
/usr/local/FlashphonerWebCallServer/client2/examples/demo/streaming/stream-auto-restore
stream-auto-restore.css - styles file
stream-auto-restore.html - client page
stream-auto-restore.js - main script to work
The example can be tested by the following URL:
https://host:8888/client2/examples/demo/streaming/stream_filter/stream-auto-restore.html
Where host - WCS server address.
Analyzing the code
To analyze the code take the file stream_filter.js version with hash 2035db9 which is available here and can be downloaded with SDK build 2.0.209.
1. Page loading action
1.1. API initialization
Flashphoner.init() code
Flashphoner.init();
1.2. Session and publishing/playing streams state objects initialization
currentSession = sessionState(); streamPublishing = streamState(); streamPlaying = streamState();
1.3. Bitrate checking object initialization
h264PublishFailureDetector = codecPublishingFailureDetector();
1.4. Connection restore object initialization
The function should be passed to the object to execute it when restore connection timer is fired
streamingRestarter = streamRestarter(function() { if (streamPublishing.wasActive) { onPublishRestart(); } if (streamPlaying.wasActive && streamPlaying.name != streamPublishing.name) { onPlayRestart(); } });
1.5. Network change detector start
networkChangeDetector();
2. Server connection/disconnection actions
2.1. Connecting to the server
Flashphoner.createSession() code
The following parameters are passed when session is created:
- url - server Websocket URL
- receiveProbes - maximum number of subsequent missing pings from server (0 disables ping checking)
- probesInterval - pings checking interval (0 disables ping checking)
Flashphoner.createSession({ urlServer: url, receiveProbes: receiveProbes, probesInterval: probesInterval }).on(SESSION_STATUS.ESTABLISHED, function (session) { ... }).on(SESSION_STATUS.DISCONNECTED, function () { ... }).on(SESSION_STATUS.FAILED, function () { ... });
2.2. Receiving the event comfirming successful connection
ConnectionStatusEvent ESTABLISHED code
When xconnection is established:
- session parameters are stored in session state object
- stream publishing/playback is restarted if stream was published/played in previous session
Flashphoner.createSession({ urlServer: url, receiveProbes: receiveProbes, probesInterval: probesInterval }).on(SESSION_STATUS.ESTABLISHED, function (session) { setStatus("#connectStatus", session.status()); currentSession.set(url, session); onConnected(session); if(restoreConnection) { if(streamPublishing.wasActive) { console.log("A stream was published before disconnection, restart publishing"); onPublishRestart(); return; } if(streamPlaying.wasActive) { console.log("A stream was played before disconnection, restart playback"); onPlayRestart(); } } }).on(SESSION_STATUS.DISCONNECTED, function () { ... }).on(SESSION_STATUS.FAILED, function () { ... });
2.3. Connection closing by clicking Disconnect button
session.disconnect() code
function onConnected(session) { $("#connectBtn").text("Disconnect").off('click').click(function () { $(this).prop('disabled', true); currentSession.isManuallyDisconnected = true; session.disconnect(); }).prop('disabled', false); ... }
2.4. Receiving the connection closing event
ConnectionStatusEvent DISCONNECTED code
If connection is closed manually by clicking Disconnect:
- state objects are cleared
- connection restore timer is stopped
Flashphoner.createSession({ urlServer: url, receiveProbes: receiveProbes, probesInterval: probesInterval }).on(SESSION_STATUS.ESTABLISHED, function (session) { ... }).on(SESSION_STATUS.DISCONNECTED, function () { setStatus("#connectStatus", SESSION_STATUS.DISCONNECTED); onDisconnected(); // Prevent streaming restart if session is manually disconnected if (currentSession.isManuallyDisconnected) { streamPublishing.clear(); streamPlaying.clear(); streamingRestarter.reset(); currentSession.clear(); } }).on(SESSION_STATUS.FAILED, function () { ... });
2.5. Receiving the connection failure event
ConnectionStatusEvent FAILED code
Connection restore timer is starting if a stream was published or played befor connection is failed
Flashphoner.createSession({ urlServer: url, receiveProbes: receiveProbes, probesInterval: probesInterval }).on(SESSION_STATUS.ESTABLISHED, function (session) { ... }).on(SESSION_STATUS.DISCONNECTED, function () { ... }).on(SESSION_STATUS.FAILED, function () { setStatus("#connectStatus", SESSION_STATUS.FAILED); onDisconnected(); if(restoreConnection && (streamPublishing.wasActive || streamPlaying.wasActive)) { streamingRestarter.restart($("#restoreTimeout").val(), $("#restoreMaxTries").val()); } });
3. Stream publishing actions
3.1 Stream publishing
session.createStream(), publish() code
The following parameters are passed while stream creation:
- streamName - stream name to publish
- localVideo - div element to display local video
- stripCodecs - codec to exclude if codec changing option is active
session.createStream({ name: streamName, display: localVideo, cacheLocalResources: true, receiveVideo: false, receiveAudio: false, stripCodecs: stripCodecs ... }).publish();
3.2. Receiving the stream publishing event
StreamStatusEvent PUBLISHING code
When stream is publishing successfully:
- bitrate checking timer starts
- the stream parameters are stored in publishing stream state object
- connection restore timer stops
- stream playback starts if stream was played previously
session.createStream({ ... }).on(STREAM_STATUS.PUBLISHING, function (stream) { setStatus("#publishStatus", STREAM_STATUS.PUBLISHING); onPublishing(stream); streamPublishing.set(streamName, stream); streamingRestarter.reset(); if ($("#restoreConnection").is(':checked') && streamPlaying.wasActive) { console.log("A stream was played before, restart playback"); onPlayRestart(); } }).on(STREAM_STATUS.UNPUBLISHED, function () { ... }).on(STREAM_STATUS.FAILED, function (stream) { ... }).publish();
3.3. Bitrate checking timer startup
function onPublishing(stream) { ... // Start publish failure detector by bitrate #WCS-3382 if($("#checkBitrate").is(':checked')) { h264PublishFailureDetector.startDetection(stream, $("#bitrateInteval").val(), $("#bitrateMaxTries").val()); } }
3.4. Publishing stopping by clicking Stop button
stream.stop() code
function onPublishing(stream) { $("#publishBtn").text("Stop").off('click').click(function () { $(this).prop('disabled', true); streamPublishing.isManuallyStopped = true; stream.stop(); }).prop('disabled', false); ... }
3.5. Receiving stream publishing stoppin event
StreamStatusEvent UNPUBLISHED code
When stream is successfully stopped:
- bitrate checking timer stops
- connection restore timer stops
- publishing stream state object is cleared
session.createStream({ ... }).on(STREAM_STATUS.PUBLISHING, function (stream) { ... }).on(STREAM_STATUS.UNPUBLISHED, function () { setStatus("#publishStatus", STREAM_STATUS.UNPUBLISHED); onUnpublished(); if (!streamPlaying.wasActive) { // No stream playback< we don't need restart any more streamingRestarter.reset(); } else if (streamPlaying.wasActive && streamPlaying.name == streamPublishing.name) { // Prevent playback restart for the same stream streamingRestarter.reset(); } streamPublishing.clear(); }).on(STREAM_STATUS.FAILED, function (stream) { ... }).publish();
3.6. Receiving stream publishing failure event
StreamStatusEvent FAILED code
When stream publishing fails:
- bitrate checking timer stops
- connection restore timer starts unless local browser error is detected (media devices unavailable for example)
session.createStream({ ... }).on(STREAM_STATUS.PUBLISHING, function (stream) { ... }).on(STREAM_STATUS.UNPUBLISHED, function () { ... }).on(STREAM_STATUS.FAILED, function (stream) { setStatus("#publishStatus", STREAM_STATUS.FAILED, stream); onUnpublished(); if ($("#restoreConnection").is(':checked') && stream.getInfo() != ERROR_INFO.LOCAL_ERROR) { streamingRestarter.restart($("#restoreTimeout").val(), $("#restoreMaxTries").val()); } }).publish();
3.7. Bitrate checking timer stopping
function onUnpublished() { ... h264PublishFailureDetector.stopDetection(streamPublishing.isManuallyStopped || currentSession.isManuallyDisconnected); ... }
4. Stream playback actions
4.1. Stream playback
session.createStream(), play() code.
The following parameters are passed while stream creation:
- streamName - stream name to play
- remoteVideo - div element to display remote video
session.createStream({ name: streamName, display: remoteVideo ... }).play();
4.2. Receiving the stream playback event
StreamStatusEvent PLAYING code
When stream is successfully playing:
- the stream parameters are stored in playing stream state object
- connection restore timer stops
session.createStream({ name: streamName, display: remoteVideo }).on(STREAM_STATUS.PENDING, function (stream) { ... }).on(STREAM_STATUS.PLAYING, function (stream) { setStatus("#playStatus", stream.status()); onPlaying(stream); streamingRestarter.reset(); streamPlaying.set(streamName, stream); }).on(STREAM_STATUS.STOPPED, function () { ... }).on(STREAM_STATUS.FAILED, function (stream) { ... }).play();
4.3 Stream playback stopping by clicking Stop button
stream.stop() code
function onPlaying(stream) { $("#playBtn").text("Stop").off('click').click(function(){ $(this).prop('disabled', true); stream.stop(); }).prop('disabled', false); $("#playInfo").text(""); }
4.4. Receiving the stream playback stopping event
StreamStatusEvent STOPPED code
When stream playback is successfully stopped:
- connection restore timer stops
- playing stream state object is cleared
session.createStream({ name: streamName, display: remoteVideo }).on(STREAM_STATUS.PENDING, function (stream) { ... }).on(STREAM_STATUS.PLAYING, function (stream) { ... }).on(STREAM_STATUS.STOPPED, function () { setStatus("#playStatus", STREAM_STATUS.STOPPED); onStopped(); streamingRestarter.reset(); streamPlaying.clear(); }).on(STREAM_STATUS.FAILED, function (stream) { ... }).play();
4.5. Receiving the stream playback failure event
StreamStatusEvent FAILED code
Connection restore timer starts is stream playback fails
session.createStream({ name: streamName, display: remoteVideo }).on(STREAM_STATUS.PENDING, function (stream) { ... }).on(STREAM_STATUS.PLAYING, function (stream) { ... }).on(STREAM_STATUS.STOPPED, function () { ... }).on(STREAM_STATUS.FAILED, function (stream) { setStatus("#playStatus", STREAM_STATUS.FAILED, stream); onStopped(); if ($("#restoreConnection").is(':checked')) { streamingRestarter.restart($("#restoreTimeout").val(), $("#restoreMaxTries").val()); } }).play();
5. Bitrate checking and stream republishing if bitrate drops to 0
5.1. Receiving browser WebRTC statistics, codec and bitrate detection, publishing stopping if bitrate drops to 0
stream.getStats(function(stat) { let videoStats = stat.outboundStream.video; if(!videoStats) { return; } let stats_codec = videoStats.codec; let bytesSent = videoStats.bytesSent; let bitrate = (bytesSent - detector.lastBytesSent) * 8; if (bitrate == 0) { detector.counter.inc(); console.log("Bitrate is 0 (" + detector.counter.getCurrent() + ")"); if (detector.counter.exceeded()) { detector.failed = true; console.log("Publishing seems to be failed, stop the stream"); stream.stop(); } } else { detector.counter.reset(); } detector.lastBytesSent = bytesSent; detector.codec = stats_codec; $("#publishInfo").text(detector.codec); });
5.2. Bitrate checking timer stopping
if (detector.publishFailureIntervalID) { clearInterval(detector.publishFailureIntervalID); detector.publishFailureIntervalID = null; }
5.3. Stream republishing
if (detector.failed) { $("#publishInfo").text("Failed to publish " + detector.codec); if($("#changeCodec").is(':checked')) { // Try to change codec from H264 to VP8 #WCS-3382 if (detector.codec == "H264") { console.log("H264 publishing seems to be failed, trying VP8 by stripping H264"); let stripCodecs = "H264"; publishBtnClick(stripCodecs); } else if (detector.codec == "VP8") { console.log("VP8 publishing seems to be failed, giving up"); } } else { // Try to republish with the same codec #WCS-3410 publishBtnClick(); } }
6. Connection restoration
6.1. Connection restore timer launching
The timer invokes a function to perform an ctions needed
restarter.restartTimerId = setInterval(function(){ if (restarter.counter.exceeded()) { logger.info("Tried to restart for " + restartMaxTimes + " times with " +restartTimeout + " ms interval, cancelled"); restarter.reset(); return; } onRestart(); restarter.counter.inc(); }, restartTimeout);
6.2. Connectin restore timer stopping
if (restarter.restartTimerId) { clearInterval(restarter.restartTimerId); logger.info("Timer " + restarter.restartTimerId + " stopped"); restarter.restartTimerId = null; } restarter.counter.reset();
6.3. New session creation if previous session is failed or disconnected
let sessions = Flashphoner.getSessions(); if (!sessions.length || sessions[0].status() == SESSION_STATUS.FAILED) { logger.info("Restart session to publish"); click("connectBtn"); } else { ... }
6.4. Republishing
let streams = sessions[0].getStreams(); let stream = null; let clickButton = false; if (streams.length == 0) { // No streams in session, try to restart publishing logger.info("No streams in session, restart publishing"); clickButton = true; } else { // If there is already a stream, check its state and restart publishing if needed for (let i = 0; i < streams.length; i++) { if (streams[i].name() === $('#publishStream').val()) { stream = streams[i]; if (!isStreamPublishing(stream)) { logger.info("Restart stream " + stream.name() + " publishing"); clickButton = true; } break; } } if (!stream) { logger.info("Restart stream publishing"); clickButton = true; } } if (clickButton) { click("publishBtn"); }
6.5. Replaying
let streams = sessions[0].getStreams(); let stream = null; let clickButton = false; if (streams.length == 0) { // No streams in session, try to restart playing logger.info("No streams in session, restart playback"); clickButton = true; } else { // If there is already a stream, check its state and restart playing if needed for (let i = 0; i < streams.length; i++) { if (streams[i].name() === $('#playStream').val()) { stream = streams[i]; if (!isStreamPlaying(stream)) { logger.info("Restart stream " + stream.name() + " playback"); clickButton = true; } break; } } if (!stream) { logger.info("Restart stream playback"); clickButton = true; } } if (clickButton) { click("playBtn"); }
7. Network change actions
7.1. Network change event handling
connection.onchange code
if (Browser.isChrome() || (Browser.isFirefox() && Browser.isAndroid())) { connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; if (connection) { connectionType = connection.type; if (Browser.isFirefox()) { connection.ontypechange = onNetworkChange; } else { connection.onchange = onNetworkChange; } } }
7.2. Closing the connection if network is changed
if (isNetworkConnected() && connection.type != connectionType) { if (currentSession.getStatus() == SESSION_STATUS.ESTABLISHED) { let logger = Flashphoner.getLogger(); logger.info("Close session due to network change from " + connectionType + " to " + connection.type); currentSession.sdkSession.disconnect(); } }