# # Kurento Rails Javascript Client # # Notes: # - Make sure that the url you pass to store video files in has sufficient space for the videos. Digital Ocean servers aren't going to cut it under heavy load. # - All of the functions in the StreamingHelper class take a callback with the signature function(error, object). Error will be null if the function call was successful. kurentoRailsHelpers = {} kurentoRailsHelpers.getRailsUrlFromBrowser = () => "#{location.hostname}/websocket" kurentoRailsHelpers.generateKurentoVideoUrl = (baseFileLocation, videoName) => date = new Date() dateString = "#{date.getMonth() + 1}-#{date.getDate()}-#{date.getFullYear()}_#{date.getHours()}:#{date.getMinutes()}:#{date.getSeconds()}" return "#{baseFileLocation}/#{encodeURIComponent(videoName.toLowerCase().replace(/\s/g, '-'))}-#{dateString}.webm" kurentoRailsHelpers.onIceCandidate = (webRtcEndpoint, webRtcPeer, onError) => webRtcPeer.on 'icecandidate', (candidate) => candidate = kurentoClient.register.complexTypes.IceCandidate(candidate) webRtcEndpoint.addIceCandidate candidate, onError webRtcEndpoint.on 'OnIceCandidate', (event) => webRtcPeer.addIceCandidate event.candidate, onError class StreamingHelper # ## Constructor - give the urls for the rails server this page was served from, as well as the kurento server we will be streaming to and from constructor: (@kurentoUrl, @railsUrl = kurentoRailsHelpers.getRailsUrlFromBrowser()) -> @kurento = kurentoClient @kurentoUrl @rails_websocket_dispatcher = new WebSocketRails @railsUrl @broadcast = {} @view = {} ### BROADCASTING FUNCTIONS AND MEMBERS ### @broadcast.kurentoObjects = {} @broadcast.kurentoObjects.pipeline = null @broadcast.kurentoObjects.webRtcPeer = null @broadcast.kurentoObjects.recorder = null @broadcast.kurentoObjects.webRtcEndpoint = null @broadcast.kurentoObjects.videoStreamId = null @broadcast.kurentoObjects.currentCallback = null onBroadcastingError = (error) => if error console.log "Broadcasting error:", error @broadcast.stopBroadcasting() @broadcast.kurentoObjects.currentCallback(error) if @broadcast.kurentoObjects.currentCallback onBroadcastingOffer = (error, offer) => return onBroadcastingError(error) if error @kurento.create 'MediaPipeline', (error, pipeline) => return onBroadcastingError(error) if error or not @broadcast.kurentoObjects.webRtcPeer @broadcast.kurentoObjects.pipeline = pipeline file_url = kurentoRailsHelpers.generateKurentoVideoUrl(@broadcast.kurentoObjects.baseFileLocation, @broadcast.kurentoObjects.videoName) mediaElements = [ {type: 'WebRtcEndpoint', params: {}} {type: 'RecorderEndpoint', params: {uri: file_url}} ] pipeline.create mediaElements, (error, elements) => return onBroadcastingError(error) if error or not @broadcast.kurentoObjects.pipeline [@broadcast.kurentoObjects.webRtcEndpoint, @broadcast.kurentoObjects.recorder] = elements kurentoRailsHelpers.onIceCandidate @broadcast.kurentoObjects.webRtcEndpoint, @broadcast.kurentoObjects.webRtcPeer, onBroadcastingError @broadcast.kurentoObjects.webRtcEndpoint.processOffer offer, (error, answer) => return onBroadcastingError(error) if error or not @broadcast.kurentoObjects.pipeline @broadcast.kurentoObjects.webRtcEndpoint.gatherCandidates onBroadcastingError @broadcast.kurentoObjects.webRtcPeer.processAnswer answer, onBroadcastingError @broadcast.kurentoObjects.webRtcEndpoint.connect @broadcast.kurentoObjects.recorder, (error) => return onBroadcastingError(error) if error or not @broadcast.kurentoObjects.pipeline @broadcast.kurentoObjects.recorder.record (error) => return onBroadcastingError(error) if error or not @broadcast.kurentoObjects.pipeline save_to_rails_success = (stream) => @broadcast.kurentoObjects.videoStreamId = stream.id @broadcast.kurentoObjects.currentCallback(null, stream) if @broadcast.kurentoObjects.currentCallback save_to_rails_failure = (stream) => @broadcast.kurentoObjects.currentCallback("Failed to save the data to rails", stream) if @broadcast.kurentoObjects.currentCallback streamToBroadcast = name: @broadcast.kurentoObjects.videoName pipeline: @broadcast.kurentoObjects.pipeline.id file_url: file_url sender_rtc: @broadcast.kurentoObjects.webRtcEndpoint.id streaming: true @rails_websocket_dispatcher.trigger 'streams.broadcast', streamToBroadcast, save_to_rails_success, save_to_rails_failure #### Broadcast (broadcast) # This function can be used to send video to the kurento server. Users listening for new streams will also be notified of the new streams creation: # # Parameters: # - videoName: string; The name of the video stream # - videoElement: DOMElement; A DOM element representing an html 5 video tag # - baseFileLocation: string; The file location on the kurento server (or an external file server) where files should be saved when streaming # - callback: function(error, object); a function you wish to have called upon either successful completion of the streaming setup process, or on the occurence of any errors # # It should probably be noted that this function not only broadcasts, but also records the stream @broadcast.broadcast = (videoName, videoElement, baseFileLocation = "file:///kurento", callback = null) => @broadcast.kurentoObjects.currentCallback = callback @broadcast.kurentoObjects.baseFileLocation = baseFileLocation @broadcast.kurentoObjects.videoName = videoName return onBroadcastingError("You must pass in a valid video tag") if videoElement is null or $(videoElement).prop("tagName").toLowerCase().trim() is not "video" videoElement = $(videoElement)[0] # a little fix to make this work if they used jquery to get the element options = { localVideo: videoElement } @broadcast.kurentoObjects.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly options, (error) -> return onBroadcastingError(error) if error this.generateOffer(onBroadcastingOffer) #### Stop Broadcasting (stopBroadcasting) # This function can be used to stop transmitting video to the kurento server. Users listening for stream changes will be notified of its completion, as will users listening for changes on this stream. # # Parameters: # - callback: function(error, object); a function you wish for us to call when we stop broadcasting successfully, or when an error occurs @broadcast.stopBroadcasting = (callback = null) => successfully_notified_rails = (stream) => @broadcast.kurentoObjects.videoStreamId = null callback() if callback failed_to_notify_rails = (stream) => console.log "Unable to update video stream status in rails" callback("Unable to update video stream status in rails") if callback if @broadcast.kurentoObjects.recorder @broadcast.kurentoObjects.recorder.stop() @broadcast.kurentoObjects.recorder = null if @broadcast.kurentoObjects.webRtcEndpoint @broadcast.kurentoObjects.webRtcEndpoint.release() @broadcast.kurentoObjects.webRtcEndpoint = null if @broadcast.kurentoObjects.currentCallback @broadcast.kurentoObjects.currentCallback = null if @broadcast.kurentoObjects.webRtcPeer @broadcast.kurentoObjects.webRtcPeer.dispose() @broadcast.kurentoObjects.webRtcPeer = null if @broadcast.kurentoObjects.pipeline @broadcast.kurentoObjects.pipeline.release() @broadcast.kurentoObjects.pipeline = null if @broadcast.kurentoObjects.videoStreamId @rails_websocket_dispatcher.trigger 'streams.stop_broadcasting', '', successfully_notified_rails, failed_to_notify_rails else callback() if callback ### END BROADCASTING FUNCTIONS AND MEMBERS ### ### VIEWING FUNCTIONS AND MEMBERS ### @view.live = {} @view.recorded = {} @view.live.kurentoObjects = {} @view.recorded.kurentoObjects = {} #### Get Active Streams (view.live.activeStreams) # This function gets an array of all of the currently active streams and passes them back in the callback you supply # Parameters: # - callback: function(error, streamObjectArray); this function will be called once either the streams are retrieved, or an error occurs @view.live.activeStreams = (callback = null, searchParams = {}) => success = (streams) => callback(null, streams) if callback error = (streams) => callback("There was an error getting the streams", streams) if callback @rails_websocket_dispatcher.trigger 'streams.active_streams', searchParams, success, error #### Subscribe to streams channel (view.live.subscribeToStreams) # This function will subscribe you to the streams channel on the rails server. Every time a new stream becomes active, the new stream handler you pass in will be called. Every time a stream terminates, the stream deactivated handler you pass in will be called. Both handlers should take the form of function(streamObject) @view.live.subscribeToStreams = (newStreamHandler, streamDeactivatedHandler) => @view.live.createChannel 'video_streams', {'new': newStreamHandler, 'remove': streamDeactivatedHandler} #### Create custom channel (view.live.createChannel) # This function will create a channel and subscribe you to it. The events parameter should be an object, where all of the keys are the names of the events (use quotes around these so that the keys work correctly with underscores and dashes and such), and the values are the handler function to be called when that event is triggered. Each handler should have have the appropriate number of arguments, for however you plan to trigger it. @view.live.createChannel = (channelName, events) => channel = @rails_websocket_dispatcher.subscribe channelName channel.bind eventName, eventHandler for own eventName, eventHandler of events #### Trigger channel event (view.live.triggerChannelEvent) # This function allows you to manually trigger a channel event from the client on any channel you would like. Any other clients who are subscribed to that event will receive the event and handle it however they've registered to do so. @view.live.triggerChannelEvent = (channelName, eventName, objectToSend) => channel = @rails_websocket_dispatcher.subscribe channelName channel.trigger eventName, objectToSend #### Subscribe to the channel for a particular video stream (view.live.onEndOfStream) # This function allows you to receive notifications when a video stream ends. When the stream ends, your endOfStreamHandler function (which should have the signature of function(streamObject)) will be called. # # Parameters: # - stream: streamObject; This is the same thing that is returned by the active streams method. Alternatively, you can pass in a regular old javascript object, so long as it has an id property. # - endOfStreamHandler: function(streamObject); This function will be called when the specified stream ends. @view.live.onEndOfStream = (stream, endOfStreamHandler) => channel = @rails_websocket_dispatcher.subscribe "stream-#{stream.id}" channel.bind 'end-of-stream', (updated_stream) => @view.live.stopViewing() endOfStreamHandler(updated_stream) onLiveViewError = (error) => if error console.log "Error!", error @view.live.stopViewing() @view.live.kurentoObjects.callback(error) if @view.live.kurentoObjects.callback onLiveViewOffer = (error, offer) => return onLiveViewError(error) if error if @view.live.kurentoObjects.pipeline and @view.live.kurentoObjects.presenterWebRtcEndpoint [pipeline, presenterEndpoint] = [@view.live.kurentoObjects.pipeline, @view.live.kurentoObjects.presenterWebRtcEndpoint] pipeline.create 'WebRtcEndpoint', (error, endpoint) => return onLiveViewError(error) if error or not @view.live.kurentoObjects.pipeline @view.live.kurentoObjects.viewerWebRtcEndpoint = endpoint kurentoRailsHelpers.onIceCandidate endpoint, @view.live.kurentoObjects.webRtcPeer, => endpoint.on 'OnIceCandidate', (event) => candidate = kurentoClient.register.complexTypes.IceCandidate event.candidate endpoint.processOffer offer, (error, answer) => return onLiveViewError(error) if error or not @view.live.kurentoObjects.pipeline @view.live.kurentoObjects.webRtcPeer.processAnswer answer, (error) => return onLiveViewError(error) if error endpoint.gatherCandidates (error) => return onLiveViewError(error) if error @view.live.kurentoObjects.presenterWebRtcEndpoint.connect endpoint, (error) => return onLiveViewError(error) if error or not @view.live.kurentoObjects.pipeline @view.live.kurentoObjects.callback(null, "Successfully started stream!") if @view.live.kurentoObjects.callback #### Start Viewing (view.live.startViewing) # This function accesses a live stream and starts it viewing. There is no error checking for if the stream is actually live, so make sure you know that it is before you start it viewing. # Parameters: # - stream: streamObject; This MUST contain at the very least the fields for sender_rtc and pipeline, though a full stream object is acceptable as well. # - videoElement: DOM Element; This should be an html 5 video tag # callback: function(error, object); The function you want to be called upon successful completion of the playback initiation process, or on error @view.live.startViewing = (stream, videoElement, callback = null) => @view.live.kurentoObjects.callback = callback return onLiveViewError("You must pass in a valid video tag") if videoElement is null or $(videoElement).prop("tagName").toLowerCase().trim() is not "video" videoElement = $(videoElement)[0] @kurento.getMediaobjectById stream.sender_rtc, (error, webRtcEndpoint) => return onLiveViewError(error) if error @view.live.kurentoObjects.presenterWebRtcEndpoint = webRtcEndpoint @kurento.getMediaobjectById stream.pipeline, (error, pipeline) => return onLiveViewError(error) if error or not @view.live.kurentoObjects.presenterWebRtcEndpoint @view.live.kurentoObjects.pipeline = pipeline options = {remoteVideo: videoElement} @view.live.kurentoObjects.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly options, (error) -> return onLiveViewError(error) if error this.generateOffer(onLiveViewOffer) #### Stop viewing (view.live.stopViewing) # This function stops whatever live stream is currently playing, then calls the callback when it has successfully stopped playback. @view.live.stopViewing = (callback = null) => if @view.live.kurentoObjects.callback @view.live.kurentoObjects.callback = null if @view.live.kurentoObjects.viewerWebRtcEndpoint @view.live.kurentoObjects.viewerWebRtcEndpoint.release() @view.live.kurentoObjects.viewerWebRtcEndpoint = null if @view.live.kurentoObjects.webRtcPeer @view.live.kurentoObjects.webRtcPeer.dispose() @view.live.kurentoObjects.webRtcPeer = null @view.live.kurentoObjects.pipeline = null if @view.live.kurentoObjects.pipeline @view.live.kurentoObjects.presenterWebRtcEndpoint = null if @view.live.kurentoObjects.presenterWebRtcEndpoint callback() if callback #### Get Available Recorded Streams (view.recorded.availableStreams) # This function returns an array of all of the available stream objects. If you want to search for more specific streams, you can also do that by using the option search params object. The callback you provide will be called when either the streams are successfully retrieved, or an error occurs. Your callback should take the form of function(error, streamsArray) @view.recorded.availableStreams = (callback = null, searchParams = {}) => success = (streams) => callback(null, streams) if callback error = (streams) => callback("There was an error getting the streams", streams) if callback @rails_websocket_dispatcher.trigger 'streams.recorded_streams', searchParams, success, error onRecordedError = (error) => console.log "Error: ", error if error @view.recorded.stopViewing() @view.recorded.kurentoObjects.callback(error) if @view.recorded.kurentoObjects.callback onRecordedOffer = (error, offer) => console.log "In the offer function" onRecordedError(error) if error @kurento.create 'MediaPipeline', (error, pipeline) => return onRecordedError(error) if error @view.recorded.kurentoObjects.pipeline = pipeline pipeline.create 'WebRtcEndpoint', (error, endpoint) => return onRecordedError(error) if error or not @view.recorded.kurentoObjects.pipeline @view.recorded.kurentoObjects.webRtcEndpoint = endpoint kurentoRailsHelpers.onIceCandidate endpoint, @view.recorded.kurentoObjects.webRtcPeer, => endpoint.processOffer offer, (error, answer) => return onRecordedError(error) if error or not @view.recorded.kurentoObjects.pipeline endpoint.gatherCandidates => @view.recorded.kurentoObjects.webRtcPeer.processAnswer answer options = {uri: @view.recorded.kurentoObjects.fileUrl} pipeline.create 'PlayerEndpoint', options, (error, player) => return onRecordedError(error) if error or not @view.recorded.kurentoObjects.pipeline player.on 'EndOfStream', (event) => channel = @rails_websocket_dispatcher.subscribe "recorded-stream-#{@view.recorded.kurentoObjects.streamId}" channel.trigger 'end-of-stream', @view.recorded.kurentoObjects.streamId @view.recorded.stopViewing() player.connect endpoint, (error) => return onRecordedError(error) if error or not @view.recorded.kurentoObjects.pipeline player.play (error) => return onRecordedError(error) if error or not @view.recorded.kurentoObjects.pipeline @view.recorded.kurentoObjects.callback() if @view.recorded.kurentoObjects.callback #### Start Viewng (view.recorded.startViewing) # This function will allow you to start viewing a recorded video stream # # Parameters: # - stream: streamObject; must contain at least the file url and the stream id. # - videoElement: dom element; must be an html 5 video tag # - callback: function(error, object) @view.recorded.startViewing = (stream, videoElement, callback = null) => console.log "Stream", stream @view.recorded.kurentoObjects.fileUrl = stream.file_url @view.recorded.kurentoObjects.streamId = stream.id @view.recorded.kurentoObjects.videoElement = videoElement @view.recorded.kurentoObjects.callback = callback return onRecordedError("You must pass in a valid video tag") if videoElement is null or $(videoElement).prop("tagName").toLowerCase().trim() is not "video" videoElement = $(videoElement)[0] options = {remoteVideo: videoElement} @view.recorded.kurentoObjects.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly options, (error) -> return onRecordedError(error) if error console.log "About to generate an offer." this.generateOffer(onRecordedOffer) #### Stop viewing (view.recorded.stopViewing) # This function will stop playback of whatever video file is currently playing # It is safe to call even if no video file is playing @view.recorded.stopViewing = (callback = null) => @view.recorded.kurentoObjects.fileUrl = null if @view.recorded.kurentoObjects.fileUrl @view.recorded.kurentoObjects.streamId = null if @view.recorded.kurentoObjects.streamId @view.recorded.kurentoObjects.callback = null if @view.recorded.kurentoObjects.callback @view.recorded.kurentoObjects.player = null if @view.recorded.kurentoObjects.player if @view.recorded.kurentoObjects.videoElement $(@view.recorded.kurentoObjects.videoElement).attr 'src', '' @view.recorded.kurentoObjects.videoElement = null if @view.recorded.kurentoObjects.webRtcEndpoint @view.recorded.kurentoObjects.webRtcEndpoint.release() @view.recorded.kurentoObjects.webRtcEndpoint = null if @view.recorded.kurentoObjects.pipeline @view.recorded.kurentoObjects.pipeline.release() @view.recorded.kurentoObjects.pipeline = null callback() if callback #### On End Of Stream (view.recorded.onEndOfStream) # This function can be used to register an end of stream handler function for a video. The stream parameter must contain at least an id, and the endOfStreamHandler should have the signature function(streamId) @view.recorded.onEndOfStream = (stream, endOfStreamHandler) => channel = @rails_websocket_dispatcher.subscribe "recorded-stream-#{stream.id}" channel.bind 'end-of-stream', (streamId) => endOfStreamHandler(streamId) #### Start Viewing (view.startViewing) # This is a helper function that makes no distinction about whether the stream you want to play is live or recorded. It will determine whether it is live for you, then call the appropriate handler function. However, an additional database call is made to determine if the stream is live, so in cases where you actually know whether the stream is live or not, you should call view.live.startViewing and view.recorded.startViewing directly. The video element should be an html5 video tag, which your video will be displayed in upon successfull completion of the viewing. As usual, your callback should take the form function(error, object) @view.startViewing = (stream, videoElement, callback = null) => live = => @view.live.startViewing(stream, videoElement, callback) recorded = => @view.recorded.startViewing(stream, videoElement, callback) @rails_websocket_dispatcher.trigger 'streams.view', '', live, recorded #### Stop Viewing (view.stopViewing) # This is a helper function that makes no distinction about whether the currently playing stream is live or recorded (or even if you have one of each playing). It will stop the streams either way. @view.stopViewing = (callback = null) => @view.live.stopViewing(callback) @view.recorded.stopViewing(callback) #### On End of Stream (view.onEndOfStream) # This is a helper function that makes no distinction about whether a stream is live or recorded. It will determine whether the stream is live or recorded for you, then call the correct endOfStream handler registration function. Note that this function does make an extra database call, though, so if you already know whether the stream is live or not, it will be faster to use view.live.onEndOfStream and view.recorded.endOfStream @view.onEndOfStream = (stream, endOfStreamHandler) => live = => @view.live.onEndOfStream(stream, endOfStreamHandler) recorded = => @view.recorded.onEndOfStream(stream, endOfStreamHandler) @rails_websocket_dispatcher.trigger 'streams.view', '', live, recorded #### Currently Viewing # This is just a simple boolean function to determine if something is currently playing # It returns true if the user is currently viewing either a live stream or a recorded one. # If the optional stream object is provided, the function will return true only if THAT specific stream is currently playing (it compares it based off of pipeline id and sender_rtc id, so make sure to pass that in). @view.currentlyViewing = (stream = null) => if stream if @view.live.kurentoObjects.pipeline and @view.live.kurentoObjects.presenterWebRtcEndpoint return @view.live.kurentoObjects.pipeline.id is stream.pipeline and @view.live.kurentoObjects.presenterWebRtcEndpoint.id is stream.sender_rtc else if @view.recorded.kurentoObjects.pipeline return @view.recorded.kurentoObjects.pipeline.id is stream.pipeline else return false return @view.live.kurentoObjects.pipeline or @view.recorded.kurentoObjects.pipeline ### END VIEWING FUNCTIONS AND MEMBERS ### ### AND FINALLY, A UNIVERSAL PAGE UNLOAD HANDLER ### # I put this on a timeout of like 2 seconds so that if the user decides to register any page unload handlers, we won't be interfering with those, and they won't be overwriting our handlers timeoutFunction = => window.addEventListener('unload', => @broadcast.stopBroadcasting() @view.stopViewing() @kurento.close() if @kurento @rails_websocket_dispatcher.disconnect() if @rails_websocket_dispatcher ) setTimeout timeoutFunction, 2000 return @ window.createStreamingHelper = (kurento_url, rails_url) => return new StreamingHelper(kurento_url, rails_url)