UNPKG

symple-client-player-fixed

Version:

Symple realtime media player - Fixed

522 lines (449 loc) 16.5 kB
// // Symple.Player.js // Media Player for the Symple Messaging Client // // Copyright (c)2010 Sourcey // http://sourcey.com // Distributed under The MIT License. // (function (S) { // Symple Player // // The abstract base class for all player implementations S.Player = S.Emitter.extend({ init: function (element, options) { this._super() this.options = S.extend({ // Default HTML template template: '\ <div class="symple-player">\ <div class="symple-player-message"></div>\ <div class="symple-player-status"></div>\ <div class="symple-player-loading"></div>\ <div class="symple-player-screen"></div>\ <div class="symple-player-controls">\ <a class="play-btn" rel="play" href="#">Play</a>\ <a class="stop-btn" rel="stop" href="#">Stop</a>\ <a class="fullscreen-btn" rel="fullscreen" href="#">Fullscreen</a>\ </div>\ </div>' }, options) this.element = element if (!S.hasClass(this.element, 'symple-player')) { this.element.innerHTML = this.options.template } if (!this.element) { throw 'Player element not found' } this.screen = this.element.getElementsByClassName('symple-player-screen')[0] if (!this.screen) { throw 'Player screen element not found' } this.message = this.element.getElementsByClassName('symple-player-message')[0] if (!this.message) { throw 'Player message element not found' } this.setup() this.bind() }, setup: function () { // virtual }, // // Player Controls // play: function (params) { // virtual this.setState('playing') }, stop: function () { // virtual this.setState('stopped') }, destroy: function () { // virtual }, mute: function (flag) { // virtual }, setError: function (state, message) { this.setState('error', message) }, setState: function (state, message) { S.log('symple:player: set state', this.state, '<=>', state) if (this.state === state) { return false } this.state = state this.displayStatus(null) this.playing = state === 'playing' if (message) { this.displayMessage(state === 'error' ? 'error' : 'info', message) } else { this.displayMessage(null) } // this.element.removeClass('state-stopped state-loading state-playing state-paused state-error') // this.element.addClass('state-' + state) this.emit('state', state, message) }, // // Helpers displayStatus: function (data) { var status = this.element.getElementsByClassName('symple-player-status')[0] if (status) { status.innerHTML = data || '' } }, // Display an overlayed player message. // Type may be one of: error, warning, info displayMessage: function (type, message) { S.log('symple:player: display message', type, message) if (message) { this.message.innerHTML = '<p class="' + type + '-message">' + message + '</p>' this.message.style.display = 'block' } else { this.message.style.display = 'none' } }, bind: function () { var self = this this.element.addEventListener('loaded', function () { self.onAction(this.rel, this) return false }) }, onAction: function (action, element) { switch (action) { case 'play': this.play() break case 'stop': this.stop() break case 'mute': this.mute(true) break case 'unmute': this.mute(false) break case 'fullscreen': this.toggleFullScreen() break default: this.emit('action', action, element) break } }, // Toggle actual player element toggleFullScreen: function () { var fullscreenElement = this.options.fullscreenElement || this.element if (S.runVendorMethod(document, 'FullScreen') || S.runVendorMethod(document, 'IsFullScreen')) { S.runVendorMethod(document, 'CancelFullScreen') } else { S.runVendorMethod(fullscreenElement, 'RequestFullScreen') } } }) // Player Engine Factory // // This factory is used to select and instantiate the best engine for the // current platform depending on supported formats and availability. S.Media = { engines: {}, // Object containing references for candidate selection register: function (engine) { S.log('symple:media: register media engine: ', engine) if (!engine.name || typeof engine.preference === 'undefined' || typeof engine.support === 'undefined') { S.log('symple:media: cannot register invalid engine', engine) return false } this.engines[engine.id] = engine return true }, has: function (id) { return typeof this.engines[id] === 'object' }, // Checks support for a given engine supports: function (id) { // Check support for engine return !!(this.has(id) && this.engines[id].support) }, // Checks support for a given format supportsFormat: function (format) { // Check support for engine return !!preferredEngine(format) }, // Returns a list of compatible engines sorted by preference // The optional format argument further filters by engines // which don't support the given media format. compatibleEngines: function (format) { var arr = [], engine // Reject non supported or disabled for (var item in this.engines) { engine = this.engines[item] if (engine.preference === 0) { continue } S.log('symple:media: supported', engine.name, engine.support) if (engine.support === true) { arr.push(engine) } } // Sort by preference arr.sort(function (a, b) { if (a.preference < b.preference) return 1 if (a.preference > b.preference) return -1 }) return arr }, // Returns the highest preference compatible engine // The optional format argument further filters by engines // which don't support the given media format. preferredEngine: function (format) { var arr = this.compatibleEngines(format) var engine = arr.length ? arr[0] : null S.log('symple:media: preferred engine', engine) return engine }, // Build URLs for the Player buildURL: function(params) { var query = [], url, addr = params.address; url = addr.scheme + '://' + addr.host + ':' + addr.port + (addr.uri ? addr.uri : '/'); for (var p in params) { if (p == 'address') continue; query.push(encodeURIComponent(p) + "=" + encodeURIComponent(params[p])); } query.push('rand=' + Math.random()); url += '?'; url += query.join("&"); return url; } } })(window.Symple = window.Symple || {}) ; // // Symple.WebRTC.js // WebRTC Player Engine for Symple // // Copyright (c)2010 Sourcey // http://sourcey.com // Distributed under The MIT License. // (function (S) { window.RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection window.RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription window.RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate window.URL = window.webkitURL || window.URL navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia S.Media.register({ id: 'WebRTC', name: 'WebRTC Player', formats: 'VP9, VP4, H.264, Opus', preference: 100, support: (function () { return typeof RTCPeerConnection !== 'undefined' })() }) S.Player.WebRTC = S.Player.extend({ init: function (element, options) { S.log('symple:webrtc: init') // Reference to the active local or remote media stream this.stream = null this._super(element, S.extend({ // Specifies that this client will be the ICE initiator, // and will be sending the initial SDP Offer. initiator: true, // The `RTCConfiguration` dictionary for the `RTCPeerConnection` rtcConfig: { iceServers: [ { url: 'stun:stun.l.google.com:19302' } ] }, // The `MediaStreamConstraints` object to pass to `getUserMedia` userMediaConstraints: { audio: true, video: true }, // The `RTCAnswerOptions` dictionary for creating the SDP offer/answer sdpConstraints: { 'mandatory': { 'OfferToReceiveAudio': true, 'OfferToReceiveVideo': true } } }, options)) }, setup: function () { S.log('symple:webrtc: setup') this._createPeerConnection() if (typeof (this.video) === 'undefined') { this.video = document.createElement('video') this.video.autoplay = true this.screen.appendChild(this.video) } }, destroy: function () { S.log('symple:webrtc: destroy') if (this.stream) { // localStream.stop() is deprecated in Chrome 45, removed in Chrome 47 if (!this.stream.stop && this.stream.getTracks) { this.stream.stop = function () { this.getTracks().forEach(function (track) { track.stop() }) } } this.stream.stop() this.stream = null } if (this.video) { this.video.src = '' this.video = null // Anything else required for video cleanup? } if (this.pc) { this.pc.close() this.pc = null // Anything else required for peer connection cleanup? } }, play: function (params) { S.log('symple:webrtc: play', params) // If there is an active stream then play it now. if (this.stream) { this.video.src = URL.createObjectURL(this.stream) this.video.play() this.setState('playing') return } // Otherwise wait until ICE to complete before setting the 'playing' state. this.setState('loading') // If we are the ICE `initiator` then attempt to open the local video // device and send the SDP Offer to the peer. if (this.options.initiator) { var self = this // TODO: Support device enumeration. S.log('symple:webrtc: initiating', this.options.userMediaConstraints) navigator.getUserMedia(this.options.userMediaConstraints, function (localStream) { // success // Play the local video stream and create the SDP offer. self.video.src = URL.createObjectURL(localStream) self.pc.addStream(localStream) self.pc.createOffer( function (desc) { // success S.log('symple:webrtc: offer', desc) self._onLocalSDP(desc) }, function (error) { // failure S.log('symple:webrtc: offer failed', error) }) // Store the active local stream self.stream = localStream }, function (error) { // failure self.setError('getUserMedia() failed: ' + error) }) } }, stop: function () { // NOTE: Stopping the player does not close the peer connection, // only `destroy` does that. This enables us to resume playback // quickly and with minimal delay. if (this.video) { this.video.src = '' // Do not nullify } // Close peer connection // if (this.pc) { // this.pc.close(); // this.pc = null; // } this.setState('stopped') }, mute: function (flag) { // Mute unless explicit false given flag = flag !== false S.log('symple:webrtc: mute', flag) if (this.video) { this.video.muted = flag } }, // Called when remote SDP is received from the peer. recvRemoteSDP: function (desc) { S.log('symple:webrtc: recv remote sdp', desc) if (!desc || !desc.type || !desc.sdp) { throw 'Invalid remote SDP' } var self = this this.pc.setRemoteDescription(new RTCSessionDescription(desc), function () { S.log('symple:webrtc: sdp success') }, function (error) { console.error('symple:webrtc: sdp error', error) self.setError('Cannot parse remote SDP offer: ' + error) }) if (desc.type === 'offer') { self.pc.createAnswer( function (answer) { // success self._onLocalSDP(answer) }, function (error) { // failure console.error('symple:webrtc: answer error', error) self.setError('Cannot create local SDP answer: ' + error) }, self.options.sdpConstraints) } }, // Called when remote candidate is received from the peer. recvRemoteCandidate: function (candidate) { S.log('symple:webrtc: recv remote candiate', candidate) if (!this.pc) { throw 'The peer connection is not initialized' } // call recvRemoteSDP first this.pc.addIceCandidate(new RTCIceCandidate(candidate)) }, // // Private methods // // Called when local SDP is ready to be sent to the peer. _onLocalSDP: function (desc) { try { this.pc.setLocalDescription(desc) this.emit('sdp', desc) } catch (e) { S.log('symple:webrtc: failed to send local SDP', e) } }, // Create the RTCPeerConnection object. _createPeerConnection: function () { if (this.pc) { throw 'The peer connection is already initialized' } S.log('symple:webrtc: create peer connnection', this.rtcConfig) var self = this this.pc = new RTCPeerConnection(this.rtcConfig) this.pc.onicecandidate = function (event) { if (event.candidate) { S.log('symple:webrtc: candidate gathered', event.candidate) self.emit('candidate', event.candidate) } else { S.log('symple:webrtc: candidate gathering complete') } } this.pc.onaddstream = function (event) { S.log('symple:webrtc: remote stream added', URL.createObjectURL(event.stream)) // Set the state to playing once candidates have completed gathering. // This is the best we can do until ICE onstatechange is implemented. self.setState('playing') self.video.src = URL.createObjectURL(event.stream) self.video.play() // Store the active stream self.stream = event.stream } this.pc.onremovestream = function (event) { S.log('symple:webrtc: remote stream removed', event) self.video.stop() self.video.src = '' } // NOTE: The following state events are still very unreliable. // Hopefully when the spec is complete this will change, but until then // we need to 'guess' the state. // this.pc.onconnecting = function(event) { S.log('symple:webrtc: onconnecting:', event); }; // this.pc.onopen = function(event) { S.log('symple:webrtc: onopen:', event); }; // this.pc.onicechange = function(event) { S.log('symple:webrtc: onicechange :', event); }; // this.pc.onstatechange = function(event) { S.log('symple:webrtc: onstatechange :', event); }; } }) // // Helpers // S.iceCandidateType = function (candidateSDP) { if (candidateSDP.indexOf('typ relay') !== -1) { return 'turn' } if (candidateSDP.indexOf('typ srflx') !== -1) { return 'stun' } if (candidateSDP.indexOf('typ host') !== -1) { return 'host' } return 'unknown' } })(window.Symple = window.Symple || {})