diff --git a/admin/app/admin/jam_ruby_users.rb b/admin/app/admin/jam_ruby_users.rb index 61a1d3db7..828e6d7f1 100644 --- a/admin/app/admin/jam_ruby_users.rb +++ b/admin/app/admin/jam_ruby_users.rb @@ -113,6 +113,21 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do @user.delete_mod(User::MOD_GEAR, User::MOD_GEAR_FRAME_OPTIONS) end + + if params[:jam_ruby_user][:how_to_use_video_no_show].to_i == 1 + @user.mod_merge({User::MOD_NO_SHOW => {User::HOWTO_USE_VIDEO_NOSHOW => true}}) + else + @user.delete_mod(User::MOD_NO_SHOW, User::HOWTO_USE_VIDEO_NOSHOW) + end + + + if params[:jam_ruby_user][:configure_video_no_show].to_i == 1 + @user.mod_merge({User::MOD_NO_SHOW => {User::CONFIGURE_VIDEO_NOSHOW=> true}}) + else + @user.delete_mod(User::MOD_NO_SHOW, User::CONFIGURE_VIDEO_NOSHOW) + end + + @user.save! redirect_to edit_admin_user_path(@user) diff --git a/admin/app/views/admin/users/_form.html.slim b/admin/app/views/admin/users/_form.html.slim index 204d9a4da..f8803324d 100644 --- a/admin/app/views/admin/users/_form.html.slim +++ b/admin/app/views/admin/users/_form.html.slim @@ -9,4 +9,7 @@ = f.input :musician = f.inputs "Gear Mods" do = f.input :show_frame_options, as: :boolean + = f.inputs "Do Not Shows" do + = f.input :how_to_use_video_no_show, as: :boolean + = f.input :configure_video_no_show, as: :boolean = f.actions diff --git a/admin/config/initializers/jam_ruby_user.rb b/admin/config/initializers/jam_ruby_user.rb index 6d7dca153..57435d83f 100644 --- a/admin/config/initializers/jam_ruby_user.rb +++ b/admin/config/initializers/jam_ruby_user.rb @@ -27,4 +27,13 @@ def show_frame_options self.get_gear_mod(MOD_GEAR_FRAME_OPTIONS) end + + + def how_to_use_video_no_show + self.get_no_show_mod(HOWTO_USE_VIDEO_NOSHOW) + end + + def configure_video_no_show + self.get_no_show_mod(CONFIGURE_VIDEO_NOSHOW) + end end diff --git a/db/Gemfile.lock b/db/Gemfile.lock index 88080fb81..62739af59 100644 --- a/db/Gemfile.lock +++ b/db/Gemfile.lock @@ -16,6 +16,3 @@ PLATFORMS DEPENDENCIES pg_migrate (= 0.1.13)! - -BUNDLED WITH - 1.10.5 diff --git a/ruby/lib/jam_ruby/jam_track_importer.rb b/ruby/lib/jam_ruby/jam_track_importer.rb index ca5c9172e..403c75330 100644 --- a/ruby/lib/jam_ruby/jam_track_importer.rb +++ b/ruby/lib/jam_ruby/jam_track_importer.rb @@ -1774,7 +1774,7 @@ module JamRuby end if count > 500 - break + #break end end diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 0e8afac88..f7d48c669 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -24,6 +24,8 @@ module JamRuby MOD_GEAR_FRAME_OPTIONS = "show_frame_options" MOD_NO_SHOW = "no_show" + HOWTO_USE_VIDEO_NOSHOW = 'how-to-use-video' + CONFIGURE_VIDEO_NOSHOW = 'configure-video' # MIN/MAX AUDIO LATENCY MINIMUM_AUDIO_LATENCY = 2 diff --git a/web/app/assets/images/content/webcam-icon-gray.png b/web/app/assets/images/content/webcam-icon-gray.png new file mode 100644 index 000000000..c2b03c469 Binary files /dev/null and b/web/app/assets/images/content/webcam-icon-gray.png differ diff --git a/web/app/assets/images/content/webcam-icon.png b/web/app/assets/images/content/webcam-icon.png new file mode 100644 index 000000000..29248197e Binary files /dev/null and b/web/app/assets/images/content/webcam-icon.png differ diff --git a/web/app/assets/javascripts/accounts_video_profile.js b/web/app/assets/javascripts/accounts_video_profile.js index 8d5a64ad7..628b1e870 100644 --- a/web/app/assets/javascripts/accounts_video_profile.js +++ b/web/app/assets/javascripts/accounts_video_profile.js @@ -4,7 +4,7 @@ context.JK = context.JK || {}; context.JK.AccountVideoProfile = function (app) { - var $webcamViewer = new context.JK.WebcamViewer() + var webcamViewerReact = null; function initialize() { var screenBindings = { 'beforeShow': beforeShow, @@ -12,16 +12,19 @@ }; app.bindScreen('account/video', screenBindings); - var $root = $("#account-video-profile .webcam-container") - $webcamViewer.init($root, true) + var reactElement = React.createElement(window.WebcamViewer, {isVisible: false}); + var reactDomNode = $("#account-video-profile .webcam-container").get(0) + webcamViewerReact = React.render(reactElement, reactDomNode) } + function beforeShow() { - $webcamViewer.beforeShow() + console.log("webcamViewerReact", webcamViewerReact) + webcamViewerReact.beforeShow() } function beforeHide() { - $webcamViewer.beforeHide() + webcamViewerReact.beforeHide() } this.beforeShow = beforeShow diff --git a/web/app/assets/javascripts/backend_alerts.js b/web/app/assets/javascripts/backend_alerts.js index 348560630..bf5b1993b 100644 --- a/web/app/assets/javascripts/backend_alerts.js +++ b/web/app/assets/javascripts/backend_alerts.js @@ -131,6 +131,12 @@ // context.JK.CurrentSessionModel.onPlaybackStateChange(type, text); context.MediaPlaybackActions.playbackStateChange(text); } + else if(type === ALERT_NAMES.VIDEO_WINDOW_OPENED) { + context.VideoActions.videoWindowOpened() + } + else if(type === ALERT_NAMES.VIDEO_WINDOW_CLOSED) { + context.VideoActions.videoWindowClosed() + } else if((!context.JK.CurrentSessionModel || !context.JK.CurrentSessionModel.inSession()) && (ALERT_NAMES.INPUT_IO_RATE == type || ALERT_NAMES.INPUT_IO_JTR == type || ALERT_NAMES.OUTPUT_IO_RATE == type || ALERT_NAMES.OUTPUT_IO_JTR== type)) { // squelch these events if not in session diff --git a/web/app/assets/javascripts/client_init.js.coffee b/web/app/assets/javascripts/client_init.js.coffee index 78f2f58b9..c84859b69 100644 --- a/web/app/assets/javascripts/client_init.js.coffee +++ b/web/app/assets/javascripts/client_init.js.coffee @@ -39,4 +39,6 @@ context.JK.ClientInit = class ClientInit nativeClientInit: () => @gearUtils.bootstrapDefaultPlaybackProfile(); + window.VideoActions.checkPromptConfigureVideo() + diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index c70d18e3b..c7ea183cf 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -80,20 +80,20 @@ } function FTUEGetVideoCaptureDeviceNames() { - return ["Built-in Webcam HD"] + return {"xy323ss": "Built-in Webcam HD"} } function FTUECurrentSelectedVideoDevice() { - return {"xy323ss": "Built-in Webcam HD"} + //return {"xy323ss": "Built-in Webcam HD"} + return {} } function FTUEGetAvailableEncodeVideoResolutions() { return { - 1 : "QCIF (176X144)", - 2 : "CIF (352X288)", - 3 : "VGA (640X480)", - 4 : "4CIF (704X576)", - 5 : "1/2WHD (640X360)", - 6: "WHD (1280X720)", - 7 : "FHD (1920x1080)" + 1 : "CIF (352X288)", + 2 : "VGA (640X480)", + 3 : "4CIF (704X576)", + 4 : "1/2WHD (640X360)", + 5 : "WHD (1280X720)", + 6 : "FHD (1920x1080)" } } diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index b8f138522..396e30be4 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -116,7 +116,16 @@ SHOW_PREFERENCES : 39, // tell frontend to show preferences dialog USB_CONNECTED : 40, // tell frontend that a USB device was connected USB_DISCONNECTED : 41, // tell frontend that a USB device was disconnected - LAST_ALERT : 42 + JAM_TRACK_SERVER_ERROR : 42, //error talking with server + BAD_INTERVAL_RATE : 43, //the audio gear is calling back at rate that does not match the expected interval + FIRST_AUDIO_PACKET : 44,// we are receiving audio from peer + NETWORK_PORT_MANGLED : 45, // packet from peer indicates network port is being mangled + NO_GLOBAL_CLOCK_SERVER : 46, //can't reach global clock NTP server + GLOBAL_CLOCK_SYNCED : 47, //clock synced + RECORDING_DONE :48, //the recording writer thread is done + VIDEO_WINDOW_OPENED :49, //video window opened + VIDEO_WINDOW_CLOSED :50, + LAST_ALERT : 51 } // recreate eThresholdType enum from MixerDialog.h context.JK.ALERT_TYPES = { @@ -171,7 +180,16 @@ 39: {"title": "", "message": ""}, // SHOW_PREFERENCES, //show preferences dialog 40: {"title": "", "message": ""}, // USB_CONNECTED 41: {"title": "", "message": ""}, // USB_DISCONNECTED, // tell frontend that a USB device was disconnected - 42: {"title": "", "message": ""} // LAST_ALERT + 42: {"title": "", "message": ""}, // JAM_TRACK_SERVER_ERROR + 43: {"title": "", "message": ""}, // BAD_INTERVAL_RATE + 44: {"title": "", "message": ""}, // FIRST_AUDIO_PACKET + 45: {"title": "", "message": ""}, // NETWORK_PORT_MANGLED + 46: {"title": "", "message": ""}, // NO_GLOBAL_CLOCK_SERVER + 47: {"title": "", "message": ""}, // GLOBAL_CLOCK_SYNCED + 48: {"title": "", "message": ""}, // RECORDING_DONE + 49: {"title": "", "message": ""}, // VIDEO_WINDOW_OPENED + 50: {"title": "", "message": ""}, // VIDEO_WINDOW_CLOSED + 51: {"title": "", "message": ""} // LAST_ALERT }; // add the alert's name to the ALERT_TYPES structure @@ -311,7 +329,9 @@ /** NAMED_MESSAGES means messages that we show to the user (dialogs/banners/whatever), that we have formally named */ context.JK.NAMED_MESSAGES = { - MASTER_VS_PERSONAL_MIX : 'master_vs_personal_mix' + MASTER_VS_PERSONAL_MIX : 'master_vs_personal_mix', + HOWTO_USE_VIDEO_NOSHOW : 'how-to-use-video', + CONFIGURE_VIDEO_NOSHOW : 'configure-video' } context.JK.ChannelGroupIds = { diff --git a/web/app/assets/javascripts/react-components.js b/web/app/assets/javascripts/react-components.js index 348d88b96..a7ca5b73f 100644 --- a/web/app/assets/javascripts/react-components.js +++ b/web/app/assets/javascripts/react-components.js @@ -12,6 +12,7 @@ //= require ./react-components/stores/SessionMyTracksStore //= require ./react-components/stores/SessionOtherTracksStore //= require ./react-components/stores/SessionMediaTracksStore +//= require ./react-components/stores/VideoStore //= require_directory ./react-components/stores //= require_directory ./react-components/mixins //= require_directory ./react-components diff --git a/web/app/assets/javascripts/react-components/PopupConfigureVideoGear.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupConfigureVideoGear.js.jsx.coffee new file mode 100644 index 000000000..7ffd1e641 --- /dev/null +++ b/web/app/assets/javascripts/react-components/PopupConfigureVideoGear.js.jsx.coffee @@ -0,0 +1,108 @@ +context = window +logger = context.JK.logger + +mixins = [] + + +# make sure this is actually us opening the window, not someone else (by checking for MixerStore) + +accessOpener = false +if window.opener? + try + m = window.opener.MixerStore + accessOpener = true + catch e + + +if accessOpener + VideoActions = window.opener.VideoActions + VideoStore = window.opener.VideoStore + +#mixins.push(Reflux.listenTo(VideoStore, 'onVideoStateChanged')) + + +@PopupConfigureVideoGear = React.createClass({ + + mixins: mixins + logger: context.JK.logger + + render: () -> + `
+
+
+

video is not configured

+
+ If you might like to use video in sessions, please select a webcam to use, and a video resolution and frame rate to capture. Then click the TEST WEBCAM button to verify that you see video from your webcam properly. In sessions, you can choose to turn video on or off any time. +
+
+ +
+ +
+ +
+
+ Important Note +
+
+ You can update your video configuration any time in your Account settings, or in the menus of the video window while in a session. +
+
+
+
+ +
+ + + + CLOSE +
+
` + + close: () -> + $root = jQuery(this.getDOMNode()) + $dontShow = $root.find('input[name="dont_show"]') + VideoActions.configureVideoPopupClosed($dontShow.is(':checked')) + window.close() + + windowUnloaded: () -> + + $root = jQuery(this.getDOMNode()) + $dontShow = $root.find('input[name="dont_show"]') + + VideoActions.howToUseVideoPopupClosed($dontShow.is(':checked')) + + componentDidMount: () -> + $(window).unload(@windowUnloaded) + + $root = jQuery(this.getDOMNode()) + + $dontShow = $root.find('input[name="dont_show"]') + context.JK.checkbox($dontShow) + + @resizeWindow() + + # this is necessary due to whatever the client's rendering behavior is. + setTimeout(@resizeWindow, 300) + + componentDidUpdate: () -> + @resizeWindow() + + resizeWindow: () => + $container = $('#minimal-container') + width = $container.width() + height = $container.height() + + # there is 20px or so of unused space at the top of the page. can't figure out why it's there. (above #minimal-container) + mysteryTopMargin = 20 + + # deal with chrome in real browsers + offset = (window.outerHeight - window.innerHeight) + mysteryTopMargin + + # handle native client chrome that the above outer-inner doesn't catch + #if navigator.userAgent.indexOf('JamKazam') > -1 + + #offset += 25 + + window.resizeTo(width, height + offset) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/PopupHowToUseVideo.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupHowToUseVideo.js.jsx.coffee new file mode 100644 index 000000000..792fee209 --- /dev/null +++ b/web/app/assets/javascripts/react-components/PopupHowToUseVideo.js.jsx.coffee @@ -0,0 +1,105 @@ +context = window +logger = context.JK.logger + +mixins = [] + + +# make sure this is actually us opening the window, not someone else (by checking for MixerStore) + +accessOpener = false +if window.opener? + try + m = window.opener.MixerStore + accessOpener = true + catch e + + +if accessOpener + VideoActions = window.opener.VideoActions + VideoStore = window.opener.VideoStore + +#mixins.push(Reflux.listenTo(VideoStore, 'onVideoStateChanged')) + + @PopupHowToUseVideo = React.createClass({ + + render: () -> + `
+
+
+ + + + Start Webcam + +
+ +
+
+ Important Note +
+
+ You can start and stop your webcam at any time by navigating to the Webcam menu of the video window and selecting Start/Stop Webcam. +
+
+
+ +
+ +
+ +
+ CLOSE +
+
` + + close: () -> + $root = jQuery(this.getDOMNode()) + $dontShow = $root.find('input[name="dont_show"]') + VideoActions.howToUseVideoPopupClosed($dontShow.is(':checked')) + window.close() + + startVideo: (e) -> + e.preventDefault + VideoActions.startVideo() + + windowUnloaded: () -> + + $root = jQuery(this.getDOMNode()) + $dontShow = $root.find('input[name="dont_show"]') + + VideoActions.howToUseVideoPopupClosed($dontShow.is(':checked')) + + componentDidMount: () -> + $(window).unload(@windowUnloaded) + + $root = jQuery(this.getDOMNode()) + + $dontShow = $root.find('input[name="dont_show"]') + context.JK.checkbox($dontShow) + + @resizeWindow() + + # this is necessary due to whatever the client's rendering behavior is. + setTimeout(@resizeWindow, 300) + + componentDidUpdate: () -> + @resizeWindow() + + resizeWindow: () => + $container = $('#minimal-container') + width = $container.width() + height = $container.height() + + # there is 20px or so of unused space at the top of the page. can't figure out why it's there. (above #minimal-container) + mysteryTopMargin = 20 + + # deal with chrome in real browsers + offset = (window.outerHeight - window.innerHeight) + mysteryTopMargin + + # handle native client chrome that the above outer-inner doesn't catch + #if navigator.userAgent.indexOf('JamKazam') > -1 + + #offset += 25 + + window.resizeTo(width, height + offset) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/actions/VideoActions.js.coffee b/web/app/assets/javascripts/react-components/actions/VideoActions.js.coffee new file mode 100644 index 000000000..5879f124f --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/VideoActions.js.coffee @@ -0,0 +1,16 @@ +context = window + +@VideoActions = Reflux.createActions({ + refresh: {} + stopVideo: {} + startVideo: {} + setVideoEncodeResolution: {} + setSendFrameRate: {} + selectDevice: {} + videoWindowOpened : {} + videoWindowClosed : {} + howToUseVideoPopupClosed: {} + toggleVideo: {} + configureVideoPopupClosed: {} + checkPromptConfigureVideo: {} +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/stores/VideoStore.js.coffee b/web/app/assets/javascripts/react-components/stores/VideoStore.js.coffee new file mode 100644 index 000000000..540ca965b --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/VideoStore.js.coffee @@ -0,0 +1,151 @@ +$ = jQuery +context = window +logger = context.JK.logger +EVENTS = context.JK.EVENTS +NAMED_MESSAGES = context.JK.NAMED_MESSAGES + +VideoActions = @VideoActions + +@VideoStore = Reflux.createStore( + { + listenables: VideoActions + logger: context.JK.logger + videoShared: false + videoOpen : false + state : null + + init: -> + this.listenTo(context.SessionStore, this.onSessionChange) + + + + # someone has requested us to refresh our config + onRefresh: -> + + currentDevice = context.jamClient.FTUECurrentSelectedVideoDevice() + deviceNames = context.jamClient.FTUEGetVideoCaptureDeviceNames() + #deviceCaps = context.jamClient.FTUEGetVideoCaptureDeviceCapabilities() + currentResolution = context.jamClient.GetCurrentVideoResolution() + currentFrameRate = context.jamClient.GetCurrentVideoFrameRate() + encodeResolutions = context.jamClient.FTUEGetAvailableEncodeVideoResolutions() + frameRates = context.jamClient.FTUEGetSendFrameRates() + + #deviceCaps: deviceCaps, + + @state = { + currentDevice: currentDevice, + deviceNames: deviceNames, + currentResolution: currentResolution, + currentFrameRate: currentFrameRate, + encodeResolutions: encodeResolutions, + frameRates: frameRates, + videoShared: @videoShared + videoOpen: @videoOpen + } + this.trigger(@state) + + onSessionChange: (@session) -> + + onStartVideo: -> + if @howtoWindow? + @howtoWindow.close() + @howtoWindow = null + #else # TESTING + # @howtoWindow = window.open("/popups/how-to-use-video", 'How to Use Video', 'scrollbars=yes,toolbar=no,status=no,height=315,width=320') + + @logger.debug("SessStartVideoSharing()") + context.jamClient.SessStartVideoSharing(0) + @videoShared = true + + @state.videoShared = @videoShared + this.trigger(@state) + + onStopVideo: -> + if @videoShared + @logger.debug("SessStopVideoSharing()") + context.jamClient.SessStopVideoSharing() + @videoShared = false + @state.videoShared = @videoShared + this.trigger(@state) + + onToggleVideo: () -> + if @videoShared + @onStopVideo() + else + @onStartVideo() + + onSetVideoEncodeResolution: (resolution) -> + context.jamClient.FTUESetVideoEncodeResolution(resolution) + + onSetSendFrameRate: (frameRates) -> + context.jamClient.FTUESetSendFrameRates(frameRates) + + onSelectDevice: (device, caps) -> + result = context.jamClient.FTUESelectVideoCaptureDevice(device, caps) + if(!result) + @logger.error("onSelectDevice failed with device #{device}") + + onVideoWindowOpened: () -> + @onRefresh() unless @state? + + @logger.debug("in session? #{@session.inSession()}, currentDevice? #{@state?.currentDevice?}, videoShared? #{@videoShared}") + + if @session.inSession() && @state.currentDevice? && Object.keys(@state.currentDevice).length > 0 && !@videoShared + context.JK.ModUtils.shouldShow(NAMED_MESSAGES.HOWTO_USE_VIDEO_NOSHOW).done((shouldShow) => + @logger.debug("checking if user has 'should show' on video howto: #{shouldShow}") + if shouldShow + @howtoWindow = window.open("/popups/how-to-use-video", 'How to Use Video', 'scrollbars=yes,toolbar=no,status=no,height=315,width=320') + ) + + #@howtoWindo.ParentRecordingStore = context.RecordingStore + #@howtoWindo.ParentIsRecording = @recordingModel.isRecording() + + @videoOpen = true + @state.videoOpen = @videoOpen + this.trigger(@state) + + onVideoWindowClosed: () -> + @onRefresh() unless @state? + + if @howtoWindow? + @howtoWindow.close() + @howtoWindow = null + + @videoOpen = false + @state.videoOpen = @videoOpen + @videoShared = false + @state.videoShared = @videoShared + this.trigger(@state) + + onHowToUseVideoPopupClosed: (dontShow) -> + if (dontShow) + @logger.debug("requesting that user no longer see how-to-use-video") + context.JK.ModUtils.updateNoShow(NAMED_MESSAGES.HOWTO_USE_VIDEO_NOSHOW); + + logger.debug("how-to-use-video popup closed") + @howtoWindow = null + + onConfigureVideoPopupClosed: (dontShow) -> + if (dontShow) + @logger.debug("requesting that user no longer see configure-video") + context.JK.ModUtils.updateNoShow(NAMED_MESSAGES.CONFIGURE_VIDEO_NOSHOW); + + logger.debug("configure-video popup closed") + @configureWindow = null + + # if the user passes all the safeguards, let's see if we should get them to configure video + onCheckPromptConfigureVideo: () -> + @onRefresh() unless @state? + + @logger.debug("checkPromptConfigureVideo", @state.currentDevice, @state.deviceNames) + + # if no device configured and this is the native client and if you have at least 1 video + if (!@state.currentDevice? || Object.keys(@state.currentDevice).length == 0) && gon?.isNativeClient && Object.keys(@state.deviceNames).length > 0 + # and if they haven't said stop bothering me about this + context.JK.ModUtils.shouldShow(NAMED_MESSAGES.CONFIGURE_VIDEO_NOSHOW).done((shouldShow) => + @logger.debug("checking if user has 'should show' on video config: #{shouldShow}") + if shouldShow + @configureWindow = window.open("/popups/configure-video", 'Configure Video', 'scrollbars=yes,toolbar=no,status=no,height=395,width=444') + ) + } +) diff --git a/web/app/assets/javascripts/react-components/stores/WebcamViewer.js.jsx.coffee b/web/app/assets/javascripts/react-components/stores/WebcamViewer.js.jsx.coffee new file mode 100644 index 000000000..d9cb7210d --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/WebcamViewer.js.jsx.coffee @@ -0,0 +1,319 @@ +context = window +logger = context.JK.logger + +reactContext = if window.opener? then window.opener else window +# make sure this is actually us opening the window, not someone else (by checking for MixerStore) +if window.opener? + try + m = window.opener.MixerStore + catch e + reactContext = window + +VideoStore = reactContext.VideoStore +VideoActions = reactContext.VideoActions + +ALERT_NAMES = context.JK.ALERT_NAMES; + +BackendToFrontend = { + 1 : "CIF (352x288)", + 2 : "VGA (640x480)", + 3 : "4CIF (704x576)", + 4 : "1/2 720p HD (640x360)", + 5 : "720p HD (1280x720)", + 6 : "1080p HD (1920x1080)" +} + +BackendNumericToBackendString = { + 1 : "CIF (352X288)", + 2 : "VGA (640X480)", + 3 : "4CIF (704X576)", + 4 : "1/2WHD (640X360)", + 5 : "WHD (1280X720)", + 6 : "FHD (1920x1080)" +} + + +BackendToFrontendFPS = { + + 1: 30, + 2: 24, + 3: 20, + 4: 15, + 5: 10 +} +FrontendToBackend = {} +for key, value of BackendToFrontend + FrontendToBackend[value] = key + +mixins = [] +mixins.push(Reflux.listenTo(VideoStore, 'onVideoStateChanged')) + + +@WebcamViewer = React.createClass({ + + mixins: mixins + logger: context.JK.logger + + getInitialState: () -> + { + currentDevice: null + deviceNames: {} + deviceCaps: null + currentResolution: 0 + currentFrameRate: 0 + encodeResolutions: {} + frameRates: {} + rescanning: false + } + + onVideoStateChanged: (changes) -> + @setState(changes) + + render: () -> + + if @props.showBackBtn + backBtn = `BACK` + + selectedDevice = this.selectedDeviceName(@state) + + # build list of webcams + + webcams = [] + context._.each @state.deviceNames, (deviceName, deviceGuid) -> + selected = deviceName == selectedDevice + webcams.push `` + + # build list of capture resolutions + + captureResolutions = [] + # load current settings from backend + currentResolution = @state.currentResolution + currentFrameRate = @state.currentFrameRate + + # protect against non-video clients pointed at video-enabled server from getting into a session + resolutions = @state.encodeResolutions + frames = @state.frameRates + @logger.debug 'FOUND THESE RESOLUTIONS', resolutions + @logger.debug 'FOUND THESE FPS', frames + context._.each resolutions, (resolution, resolutionKey, obj) => + + #{1: "CIF (352X288)", 2: "VGA (640X480)", 3: "4CIF (704X576)", 4: "1/2WHD (640X360)", 5: "WHD (1280X720)", 6: "FHD (1920x1080)"} + context._.each frames, (frame, key, obj) => + + frontendResolution = BackendToFrontend[resolutionKey] + + @logger.error("unknown resolution! #{resolution}", BackendToFrontend) unless frontendResolution + + value = "#{resolutionKey}|#{frame}" + text = "#{frontendResolution} at #{frame} fps" + + selected = currentResolution + '|' + currentFrameRate == value + + captureResolutions.push `` + + autoSelect = false + if currentResolution == 0 + @logger.warn("current resolution not specified; defaulting to VGA") + autoSelect = true + currentResolution = 2 + if currentFrameRate == 0 + autoSelect = true + @logger.warn("current frame rate not specified; defaulting to 30") + currentFrameRate = 30 + else + convertedFrameRate = BackendToFrontendFPS[currentFrameRate] + @logger.debug("translating FPS: backend numeric #{currentFrameRate} to #{convertedFrameRate}") + currentFrameRate = convertedFrameRate + + # backend needs to be same as frontend + if autoSelect + @updateBackend(currentResolution, currentFrameRate) + + if @state.videoShared + toggleText = 'STOP WEBCAM' + else + toggleText = 'TEST WEBCAM' + + if @state.rescanning + rescanning = + ` + + CHECKING GEAR + ` + + `
+

select webcam:

+
+ +
+

select video capture resolution & frame rate:

+
+ + [?] +
+
+ {backBtn} + {toggleText} +
+ {rescanning} +
` + + componentDidMount: () -> + + if @props.isVisible + @beforeShow() + + $root = $(@getDOMNode()) + $videoSettingsHelp = $root.find('.ftue-video-settings-help') + context.JK.helpBubble($videoSettingsHelp, 'ftue-video-settings', {}, {width:300}) if $videoSettingsHelp.length > 0 + $videoSettingsHelp.click(false) + + componentWillUpdate: (nextProps, nextState) -> + # protect against non-video clients pointed at video-enabled server from getting into a session + + @logger.debug("webcam devices", nextState.deviceNames, @state.deviceNames) + + if !@initialScan? + @initialScan = true + else + @findChangedWebcams(nextState.deviceNames, @state.deviceNames) + + componentWillReceiveProps:(nextProps) -> + if nextProps.isVisible + @beforeShow() + else + @beforeHide() + + beforeShow:() -> + + VideoActions.refresh() + VideoActions.stopVideo() + + context.JK.onBackendEvent(ALERT_NAMES.USB_CONNECTED, 'webcam-viewer', @onUsbDeviceConnected); + context.JK.onBackendEvent(ALERT_NAMES.USB_DISCONNECTED, 'webcam-viewer', @onUsbDeviceDisconnected); + + beforeHide: () -> + + context.JK.offBackendEvent(ALERT_NAMES.USB_CONNECTED, 'webcam-viewer', @onUsbDeviceConnected); + context.JK.offBackendEvent(ALERT_NAMES.USB_DISCONNECTED, 'webcam-viewer', @onUsbDeviceDisconnected); + + if @rescanTimeout? + clearTimeout(@rescanTimeout) + @rescanTimeout = null + + @setVideoOff() + + + onUsbDeviceConnected: () -> + # don't handle USB events when minimized + #return if !context.jamClient.IsFrontendVisible() + + logger.debug("USB device connected") + + @scheduleRescanSystem(3000) + + onUsbDeviceDisconnected:() -> + # don't handle USB events when minimized + #return if !context.jamClient.IsFrontendVisible() + + logger.debug("USB device disconnected") + + @scheduleRescanSystem(3000) + + scheduleRescanSystem: (time) -> + if @rescanTimeout? + clearTimeout(@rescanTimeout) + @rescanTimeout = null + + @setState({rescanning: true}) + @rescanTimeout = setTimeout(() => + @setState({rescanning: false}) + VideoActions.refresh() + , time) + + selectWebcam:(e) -> + e.preventDefault() + + device = $(e.target).val() + + VideoActions.selectDevice(device, {}) + + updateBackend: (selectedResolution, selectedFps) -> + @logger.debug 'Selecting webcam resolution: ', selectedResolution + @logger.debug 'Selecting webcam fps: ', selectedFps + + VideoActions.setVideoEncodeResolution(selectedResolution) + VideoActions.setSendFrameRate(selectedFps) + + selectResolution:(e) -> + e.preventDefault() + + resolution = $(e.target).val() + @logger.debug 'new capture resolution selected: ' + resolution + + if resolution? + bits = resolution.split('|') + selectedResolution = bits[0] + selectedFps = bits[1] + @updateBackend(selectedResolution, selectedFps) + + setVideoOff:() -> + VideoActions.stopVideo() + + back: () => + window.location = '/client#/account' + + toggleWebcam:(e) -> + e.preventDefault() + $toggleBtn = $(e.target) + VideoActions.toggleVideo() + + #if this.isVideoShared() + # $toggleBtn.removeClass("selected") + # VideoActions.stopVideo() + # @setState({videoShared: false}) + #else + # $toggleBtn.addClass("selected") + # VideoActions.startVideo() + # @setState({videoShared: true}) + + selectedDeviceName:(state) -> + webcamName="None Configured" + # protect against non-video clients pointed at video-enabled server from getting into a session + webcam = state.currentDevice + @logger.debug("currently selected video device", webcam) + if (webcam? && Object.keys(webcam).length>0) + webcamName = Object.keys(webcam)[0] + + webcamName + + findChangedWebcams: (newList, oldList) -> + newKeys = Object.keys(newList) + oldKeys = Object.keys(oldList) + + webcamSelect = $(@getDOMNode()).find('.webcam-select-container select') + + if newKeys.length > oldKeys.length + for newKey in newKeys + if oldKeys.indexOf(newKey) == -1 + newWebcam = newList[newKey] + @logger.debug("new webcam found: " + newWebcam, newKey) + context.JK.prodBubble(webcamSelect, 'new-webcam-found', {name: newWebcam}, {positions:['right']}) + break + else if newKeys.length < oldKeys.length + for oldKey in oldKeys + if newKeys.indexOf(oldKey) == -1 + oldWebcam = oldList[oldKey] + @logger.debug("webcam no longer found: " + oldWebcam) + context.JK.prodBubble(webcamSelect, 'old-webcam-lost', {name: oldWebcam}, {positions:['right']}) + break + + + } +) + + diff --git a/web/app/assets/javascripts/webcam_viewer.js.coffee b/web/app/assets/javascripts/webcam_viewer.js.coffee index 71a9ce41d..22cff4bb2 100644 --- a/web/app/assets/javascripts/webcam_viewer.js.coffee +++ b/web/app/assets/javascripts/webcam_viewer.js.coffee @@ -2,6 +2,7 @@ $ = jQuery context = window context.JK ||= {}; + ALERT_NAMES = context.JK.ALERT_NAMES; BackendToFrontend = { diff --git a/web/app/assets/javascripts/wizard/gear/step_video_gear.js b/web/app/assets/javascripts/wizard/gear/step_video_gear.js index b02bcd89a..1c502713d 100644 --- a/web/app/assets/javascripts/wizard/gear/step_video_gear.js +++ b/web/app/assets/javascripts/wizard/gear/step_video_gear.js @@ -4,21 +4,24 @@ context.JK = context.JK || {} context.JK.StepVideoGear = function (app, $dialog) { - var $step = null - var $webcamViewer = new context.JK.WebcamViewer() + var $step = null + var webcamViewerReact = null; + function initialize(_$step) { $step = _$step - $webcamViewer.init($step, false) + var reactElement = React.createElement(window.WebcamViewer, {isVisible: false}); + var reactDomNode = $step.find(".webcam-container").get(0) + webcamViewerReact = React.render(reactElement, reactDomNode) } function beforeShow() { $dialog.getWizard().getDialog().find('h1.top-header').text('video gear setup') - $webcamViewer.beforeShow() + webcamViewerReact.beforeShow() } function beforeHide() { $dialog.getWizard().getDialog().find('h1.top-header').text('audio gear setup') - $webcamViewer.beforeHide() + webcamViewerReact.beforeHide() } this.beforeShow = beforeShow diff --git a/web/app/assets/stylesheets/client/account.css.scss b/web/app/assets/stylesheets/client/account.css.scss index 30ed3c430..dccd4d236 100644 --- a/web/app/assets/stylesheets/client/account.css.scss +++ b/web/app/assets/stylesheets/client/account.css.scss @@ -73,7 +73,6 @@ .content-wrapper.account-video { .rescanning-notice { - display:none; span.spinner-small { display:inline-block; diff --git a/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss b/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss index cba90acc8..65063a885 100644 --- a/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss +++ b/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss @@ -319,7 +319,6 @@ .wizard-step.video-gear { .wizard-step-content .wizard-step-column { form.video .rescanning-notice { - display:none; margin-top: 20px; margin-left: -5px; diff --git a/web/app/assets/stylesheets/minimal/configure_video_gear.css.scss b/web/app/assets/stylesheets/minimal/configure_video_gear.css.scss new file mode 100644 index 000000000..494974a02 --- /dev/null +++ b/web/app/assets/stylesheets/minimal/configure_video_gear.css.scss @@ -0,0 +1,173 @@ +@import "client/common"; + +body.configure-video-popup { + + position:relative; + color: $ColorTextTypical; + + #minimal-container { + padding-bottom:20px; + } + + .popup-contents { + width:350px; + margin-top:20px; + padding-left:44px; + padding-right:44px; + } + + .control-holder { + width:100%; + margin: 1em 0; + } + + .helper { + display: inline-block; + height: 100%; + vertical-align: middle; + } + + .control { + width:231px; + height:34px; + @include border_box_sizing; + margin-top:15px; + padding:3px; + background-color:#242323; + text-align:center; + font-size:13px; + border-radius:5px; + vertical-align:middle; + color:#ccc; + } + + + .control img { + vertical-align:middle; + margin-right:5px; + } + + .control span { + vertical-align:middle; + } + + .iradio_minimal { + float:left; + margin-right:5px; + } + + label { + padding-top:2px; + } + + .field { + height:18px; + &:nth-child(1) { + + } + &:nth-child(2) { + margin-top:9px; + } + } + + h5 { + text-decoration:underline; + margin-bottom:5px; + } + + .important-note { + margin-top:60px; + line-height:150%; + font-size:12px; + } + + .close-behavior { + + margin-bottom: 10px; + text-align: center; + font-size:11px; + + position:relative; + + .icheckbox_minimal { + top: 4px; + margin-right: 5px; + } + + .field { + position:absolute; + left: 43px; + top: 16px; + } + + label { + display:inline; + } + } + .close-link { + margin-top:20px; + font-size:11px; + } + + .rescanning-notice { + + span.spinner-small { + display:inline-block; + vertical-align: middle; + } + } + + h2.subcaption { + color:white; + font-size: 23px; + font-weight: 400; + margin-bottom:20px !important; + } + + div.subcaption { + line-height:150%; + font-size:12px; + } + + .sub-header { + color:white; + font-size: 16px; + font-weight: 400; + margin-bottom:2px; + } + + .webcam-container { + margin-top:40px; + } + + .webcam-test-btn { + margin-right: 2px; + margin-top: 4px; + } + + select { + @include border_box_sizing; + width:350px; + + } + + form { + @include border_box_sizing; + width:350px; + } + + a.ftue-video-settings-help { + margin-left:15px; + + position: absolute; + margin-top: 3px; + } + + .configure-webcam { + float:right; + } + + .wizard_control { + margin-bottom: 10px; + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/minimal/how_to_use_video.css.scss b/web/app/assets/stylesheets/minimal/how_to_use_video.css.scss new file mode 100644 index 000000000..100c7db94 --- /dev/null +++ b/web/app/assets/stylesheets/minimal/how_to_use_video.css.scss @@ -0,0 +1,110 @@ +@import "client/common"; + +body.how-to-use-video-popup { + + position:relative; + color: $ColorTextTypical; + + #minimal-container { + padding-bottom:20px; + } + + .popup-contents { + padding-left:44px; + padding-right:44px; + } + + .control-holder { + width:100%; + margin: 1em 0; + } + + .helper { + display: inline-block; + height: 100%; + vertical-align: middle; + } + + .control { + width:231px; + height:34px; + @include border_box_sizing; + margin-top:15px; + padding:3px; + background-color:#242323; + text-align:center; + font-size:13px; + border-radius:5px; + vertical-align:middle; + color:#ccc; + } + + + .control img { + vertical-align:middle; + margin-right:5px; + margin-top:-2px; + } + + .control span { + vertical-align:middle; + } + + .iradio_minimal { + float:left; + margin-right:5px; + } + + label { + padding-top:2px; + } + + .field { + height:18px; + } + + .note-show-hide { + font-size:11px; + } + + h5 { + text-decoration:underline; + margin-bottom:5px; + } + + .important-note { + margin-top:30px; + line-height:150%; + font-size:12px; + } + + + .field { + margin-top:15px; + font-size:11px; + padding-left:44px; + .icheckbox_minimal { + top: 4px; + margin-right: 5px; + } + + label { + display:inline; + } + } + + .close-behavior { + + position:relative; + margin-top:10px; + margin-bottom:10px; + font-size:11px; + text-align:center; + + + } + .close-link { + margin-top:20px; + font-size:11px; + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/minimal/minimal.css.scss b/web/app/assets/stylesheets/minimal/minimal.css.scss index 6ee69d0f5..5ac30bc35 100644 --- a/web/app/assets/stylesheets/minimal/minimal.css.scss +++ b/web/app/assets/stylesheets/minimal/minimal.css.scss @@ -5,6 +5,7 @@ *= require client/screen_common *= require client/content *= require client/ftue +*= require client/help *= require icheck/minimal/minimal *= require_directory . *= require client/metronomePlaybackModeSelect diff --git a/web/app/controllers/popups_controller.rb b/web/app/controllers/popups_controller.rb index 21c3693b3..e1a6c95c4 100644 --- a/web/app/controllers/popups_controller.rb +++ b/web/app/controllers/popups_controller.rb @@ -10,6 +10,14 @@ class PopupsController < ApplicationController render :layout => "minimal" end + def how_to_use_video + render :layout => "minimal" + end + + def configure_video + render :layout => "minimal" + end + def youtube_player @video_id = params[:id] render :layout => "minimal" diff --git a/web/app/views/clients/_account_video_profile.html.erb b/web/app/views/clients/_account_video_profile.html.erb index 31b2d20c0..8d1de1e2b 100644 --- a/web/app/views/clients/_account_video_profile.html.erb +++ b/web/app/views/clients/_account_video_profile.html.erb @@ -21,12 +21,11 @@

video gear:

- Select webcam to use for video in sessions. Verify that you see video from webcam in the external application window (it may be behind this window). Configure webcam settings if desired. + Select webcam to use for video in sessions. Verify that you see video from webcam in the external application window (it may be behind this window).
- <%= render 'webcam' %>
diff --git a/web/app/views/clients/wizard/gear/_video_gear.html.haml b/web/app/views/clients/wizard/gear/_video_gear.html.haml index 888067af9..452b9a615 100644 --- a/web/app/views/clients/wizard/gear/_video_gear.html.haml +++ b/web/app/views/clients/wizard/gear/_video_gear.html.haml @@ -8,7 +8,7 @@ %li Select video capture settings. %li Click the Test webcam button to open a window and verify that your webcam is properly capturing video. Then use the Window / Close Video Window menu command to close the window, and click the Next button to move forward. .wizard-step-column - =render(partial: '/clients/webcam') + .webcam-container .clearall / Webcam from client can't currently be embedded: / .wizard-step-column diff --git a/web/app/views/layouts/minimal.html.erb b/web/app/views/layouts/minimal.html.erb index 3a5ad3ea9..5e7e80320 100644 --- a/web/app/views/layouts/minimal.html.erb +++ b/web/app/views/layouts/minimal.html.erb @@ -25,6 +25,7 @@ <%= yield %> + <%= render "clients/help" %>