Merge branch 'develop' into feature/recording_selector

This commit is contained in:
Jonathan Kolyer 2015-02-16 02:20:38 +00:00
commit 6a46ad5910
72 changed files with 3454 additions and 452 deletions

View File

@ -246,4 +246,9 @@ text_message_migration.sql
user_model_about_changes.sql
performance_samples.sql
user_presences.sql
discard_scores_optimized.sql
discard_scores_optimized.sql
backing_tracks.sql
metronome.sql
recorded_backing_tracks.sql
recorded_backing_tracks_add_filename.sql
user_syncs_include_backing_tracks.sql

2
db/up/backing_tracks.sql Normal file
View File

@ -0,0 +1,2 @@
ALTER TABLE active_music_sessions ADD COLUMN backing_track_path VARCHAR(1024);
ALTER TABLE active_music_sessions ADD COLUMN backing_track_initiator_id VARCHAR(64);

2
db/up/metronome.sql Normal file
View File

@ -0,0 +1,2 @@
ALTER TABLE active_music_sessions ADD COLUMN metronome_active BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE active_music_sessions ADD COLUMN metronome_initiator_id VARCHAR(64);

View File

@ -0,0 +1,38 @@
CREATE UNLOGGED TABLE backing_tracks (
id VARCHAR(64) NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(),
filename VARCHAR(1024) NOT NULL,
connection_id VARCHAR(64) NOT NULL REFERENCES connections(id) ON DELETE CASCADE,
client_track_id VARCHAR(64) NOT NULL,
client_resource_id VARCHAR(100),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE recorded_backing_tracks (
id BIGINT PRIMARY KEY,
user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE,
backing_track_id VARCHAR(64),
recording_id VARCHAR(64) NOT NULL,
client_track_id VARCHAR(64) NOT NULL,
is_part_uploading BOOLEAN NOT NULL DEFAULT FALSE,
next_part_to_upload INTEGER NOT NULL DEFAULT 0,
upload_id CHARACTER VARYING(1024),
part_failures INTEGER NOT NULL DEFAULT 0,
discard BOOLEAN,
download_count INTEGER NOT NULL DEFAULT 0,
md5 CHARACTER VARYING(100),
length BIGINT,
client_id VARCHAR(64) NOT NULL,
file_offset BIGINT,
url VARCHAR(1024) NOT NULL,
fully_uploaded BOOLEAN NOT NULL DEFAULT FALSE,
upload_failures INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE recorded_backing_tracks ALTER COLUMN id SET DEFAULT nextval('tracks_next_tracker_seq');

View File

@ -0,0 +1,2 @@
ALTER TABLE recorded_backing_tracks ADD COLUMN filename VARCHAR NOT NULL;
ALTER TABLE recorded_backing_tracks ADD COLUMN last_downloaded_at TIMESTAMP WITHOUT TIME ZONE;

View File

@ -0,0 +1,47 @@
DROP VIEW user_syncs;
CREATE VIEW user_syncs AS
SELECT DISTINCT b.id AS recorded_track_id,
CAST(NULL as BIGINT) AS mix_id,
CAST(NULL as BIGINT) AS quick_mix_id,
CAST(NULL as BIGINT) AS recorded_backing_track_id,
b.id AS unified_id,
a.user_id AS user_id,
b.fully_uploaded,
recordings.created_at AS created_at,
recordings.id AS recording_id
FROM recorded_tracks a INNER JOIN recordings ON a.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE INNER JOIN recorded_tracks b ON a.recording_id = b.recording_id
UNION ALL
SELECT CAST(NULL AS BIGINT) AS recorded_track_id,
CAST(NULL as BIGINT) AS mix_id,
CAST(NULL as BIGINT) AS quick_mix_id,
a.id AS recorded_backing_track_id,
a.id AS unified_id,
a.user_id AS user_id,
a.fully_uploaded,
recordings.created_at AS created_at,
recordings.id AS recording_id
FROM recorded_backing_tracks a INNER JOIN recordings ON a.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE
UNION ALL
SELECT CAST(NULL as BIGINT) AS recorded_track_id,
mixes.id AS mix_id,
CAST(NULL as BIGINT) AS quick_mix_id,
CAST(NULL as BIGINT) AS recorded_backing_track_id,
mixes.id AS unified_id,
claimed_recordings.user_id AS user_id,
NULL as fully_uploaded,
recordings.created_at AS created_at,
recordings.id AS recording_id
FROM mixes INNER JOIN recordings ON mixes.recording_id = recordings.id INNER JOIN claimed_recordings ON recordings.id = claimed_recordings.recording_id WHERE claimed_recordings.discarded = FALSE AND deleted = FALSE
UNION ALL
SELECT CAST(NULL as BIGINT) AS recorded_track_id,
CAST(NULL as BIGINT) AS mix_id,
quick_mixes.id AS quick_mix_id,
CAST(NULL as BIGINT) AS recorded_backing_track_id,
quick_mixes.id AS unified_id,
quick_mixes.user_id,
quick_mixes.fully_uploaded,
recordings.created_at AS created_at,
recordings.id AS recording_id
FROM quick_mixes INNER JOIN recordings ON quick_mixes.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE;

View File

@ -430,7 +430,7 @@ def assert_all_tracks_seen(users=[])
users.each do |user|
in_client(user) do
users.reject {|u| u==user}.each do |other|
find('div.track-label', text: other.name)
find('div.track-label > span', text: other.name)
#puts user.name + " is able to see " + other.name + "\'s track"
end
end

View File

@ -87,6 +87,7 @@ require "jam_ruby/lib/stats.rb"
require "jam_ruby/amqp/amqp_connection_manager"
require "jam_ruby/database"
require "jam_ruby/message_factory"
require "jam_ruby/models/backing_track"
require "jam_ruby/models/feedback"
require "jam_ruby/models/feedback_observer"
#require "jam_ruby/models/max_mind_geo"
@ -132,8 +133,11 @@ require "jam_ruby/models/search"
require "jam_ruby/models/recording"
require "jam_ruby/models/recording_comment"
require "jam_ruby/models/recording_liker"
require "jam_ruby/models/recorded_backing_track"
require "jam_ruby/models/recorded_backing_track_observer"
require "jam_ruby/models/recorded_track"
require "jam_ruby/models/recorded_track_observer"
require "jam_ruby/models/recorded_video"
require "jam_ruby/models/quick_mix"
require "jam_ruby/models/quick_mix_observer"
require "jam_ruby/models/share_token"
@ -197,7 +201,6 @@ require "jam_ruby/models/score_history"
require "jam_ruby/models/jam_company"
require "jam_ruby/models/user_sync"
require "jam_ruby/models/video_source"
require "jam_ruby/models/recorded_video"
require "jam_ruby/models/text_message"
require "jam_ruby/jam_tracks_manager"

View File

@ -82,6 +82,8 @@ module ValidationMessages
MUST_BE_KNOWN_TIMEZONE = "not valid"
JAM_TRACK_ALREADY_OPEN = 'another jam track already open'
RECORDING_ALREADY_IN_PROGRESS = "recording being made"
METRONOME_ALREADY_OPEN = 'another metronome already open'
BACKING_TRACK_ALREADY_OPEN = 'another audio file already open'
# notification
DIFFERENT_SOURCE_TARGET = 'can\'t be same as the sender'

View File

@ -7,7 +7,7 @@ module JamRuby
self.table_name = 'active_music_sessions'
attr_accessor :legal_terms, :max_score, :opening_jam_track, :opening_recording
attr_accessor :legal_terms, :max_score, :opening_jam_track, :opening_recording, :opening_backing_track, :opening_metronome
belongs_to :claimed_recording, :class_name => "JamRuby::ClaimedRecording", :foreign_key => "claimed_recording_id", :inverse_of => :playing_sessions
belongs_to :claimed_recording_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_claimed_recordings, :foreign_key => "claimed_recording_initiator_id"
@ -15,6 +15,9 @@ module JamRuby
belongs_to :jam_track, :class_name => "JamRuby::JamTrack", :foreign_key => "jam_track_id", :inverse_of => :playing_sessions
belongs_to :jam_track_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "jam_track_initiator_id"
belongs_to :backing_track_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "backing_track_initiator_id"
belongs_to :metronome_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "metronome_initiator_id"
has_one :music_session, :class_name => "JamRuby::MusicSession", :foreign_key => 'music_session_id'
has_one :mount, :class_name => "JamRuby::IcecastMount", :inverse_of => :music_session, :foreign_key => 'music_session_id'
belongs_to :creator, :class_name => 'JamRuby::User', :foreign_key => :user_id
@ -27,6 +30,8 @@ module JamRuby
validate :creator_is_musician
validate :validate_opening_recording, :if => :opening_recording
validate :validate_opening_jam_track, :if => :opening_jam_track
validate :validate_opening_backing_track, :if => :opening_backing_track
validate :validate_opening_metronome, :if => :opening_metronome
after_create :started_session
@ -73,22 +78,52 @@ module JamRuby
if is_jam_track_open?
errors.add(:claimed_recording, ValidationMessages::JAM_TRACK_ALREADY_OPEN)
end
if is_backing_track_open?
errors.add(:claimed_recording, ValidationMessages::BACKING_TRACK_ALREADY_OPEN)
end
if is_metronome_open?
errors.add(:claimed_recording, ValidationMessages::METRONOME_ALREADY_OPEN)
end
end
def validate_opening_jam_track
validate_other_audio(:jam_track)
end
def validate_opening_backing_track
validate_other_audio(:backing_track)
end
def validate_opening_metronome
validate_other_audio(:metronome)
end
def validate_other_audio(error_key)
# validate that there is no metronome already open in this session
if metronome_active_was
errors.add(error_key, ValidationMessages::METRONOME_ALREADY_OPEN)
end
# validate that there is no backing track already open in this session
if backing_track_path_was.present?
errors.add(error_key, ValidationMessages::BACKING_TRACK_ALREADY_OPEN)
end
# validate that there is no jam track already open in this session
unless jam_track_id_was.nil?
errors.add(:jam_track, ValidationMessages::JAM_TRACK_ALREADY_OPEN)
if jam_track_id_was.present?
errors.add(error_key, ValidationMessages::JAM_TRACK_ALREADY_OPEN)
end
# validate that there is no recording being made
if is_recording?
errors.add(:jam_track, ValidationMessages::RECORDING_ALREADY_IN_PROGRESS)
errors.add(error_key, ValidationMessages::RECORDING_ALREADY_IN_PROGRESS)
end
# validate that there is no recording being played back to the session
if is_playing_recording?
errors.add(:jam_track, ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS)
errors.add(error_key, ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS)
end
end
@ -593,6 +628,14 @@ module JamRuby
!self.jam_track.nil?
end
def is_backing_track_open?
self.backing_track_path.present?
end
def is_metronome_open?
self.metronome_active.present?
end
# is this music session currently recording?
def is_recording?
recordings.where(:duration => nil).count > 0
@ -742,6 +785,35 @@ module JamRuby
self.save
end
# @param backing_track_path is a relative path:
def open_backing_track(user, backing_track_path)
self.backing_track_path = backing_track_path
self.backing_track_initiator = user
self.opening_backing_track = true
self.save
self.opening_backing_track = false
end
def close_backing_track
self.backing_track_path = nil
self.backing_track_initiator = nil
self.save
end
def open_metronome(user)
self.metronome_active = true
self.metronome_initiator = user
self.opening_metronome = true
self.save
self.opening_metronome = false
end
def close_metronome
self.metronome_active = false
self.metronome_initiator = nil
self.save
end
def self.sync(session_history)
music_session = MusicSession.find_by_id(session_history.id)

View File

@ -0,0 +1,21 @@
module JamRuby
class BackingTrack < ActiveRecord::Base
self.table_name = "backing_tracks"
self.primary_key = 'id'
default_scope order('created_at ASC')
belongs_to :connection, :class_name => "JamRuby::Connection", :inverse_of => :tracks, :foreign_key => 'connection_id'
validates :connection, presence: true
validates :client_track_id, presence: true
validates :filename, presence: true
def user
self.connection.user
end
end
end

View File

@ -18,6 +18,7 @@ module JamRuby
belongs_to :music_session, :class_name => "JamRuby::ActiveMusicSession", foreign_key: :music_session_id
has_one :latency_tester, class_name: 'JamRuby::LatencyTester', foreign_key: :client_id, primary_key: :client_id
has_many :tracks, :class_name => "JamRuby::Track", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all
has_many :backing_tracks, :class_name => "JamRuby::BackingTrack", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all
has_many :video_sources, :class_name => "JamRuby::VideoSource", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all
validates :as_musician, :inclusion => {:in => [true, false, nil]}

View File

@ -138,11 +138,17 @@ module JamRuby
manifest = { "files" => [], "timeline" => [] }
mix_params = []
recording.recorded_tracks.each do |recorded_track|
manifest["files"] << { "filename" => recorded_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 }
mix_params << { "level" => 100, "balance" => 0 }
end
recording.recorded_backing_tracks.each do |recorded_backing_track|
manifest["files"] << { "filename" => recorded_backing_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 }
mix_params << { "level" => 100, "balance" => 0 }
end
manifest["timeline"] << { "timestamp" => 0, "mix" => mix_params }
manifest["output"] = { "codec" => "vorbis" }
manifest["recording_id"] = self.recording.id

View File

@ -0,0 +1,196 @@
module JamRuby
# BackingTrack analog to JamRuby::RecordedTrack
class RecordedBackingTrack < ActiveRecord::Base
include JamRuby::S3ManagerMixin
attr_accessor :marking_complete
attr_writer :current_user
belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :recorded_backing_tracks
belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :recorded_backing_tracks
validates :filename, :presence => true
validates :client_id, :presence => true # not a connection relation on purpose
validates :backing_track_id, :presence => true # not a track relation on purpose
validates :client_track_id, :presence => true
validates :md5, :presence => true, :if => :upload_starting?
validates :length, length: {minimum: 1, maximum: 1024 * 1024 * 256 }, if: :upload_starting? # 256 megs max. is this reasonable? surely...
validates :user, presence: true
validates :download_count, presence: true
before_destroy :delete_s3_files
validate :validate_fully_uploaded
validate :validate_part_complete
validate :validate_too_many_upload_failures
validate :verify_download_count
def self.create_from_backing_track(backing_track, recording)
recorded_backing_track = self.new
recorded_backing_track.recording = recording
recorded_backing_track.client_id = backing_track.connection.client_id
recorded_backing_track.backing_track_id = backing_track.id
recorded_backing_track.client_track_id = backing_track.client_track_id
recorded_backing_track.user = backing_track.connection.user
recorded_backing_track.filename = backing_track.filename
recorded_backing_track.next_part_to_upload = 0
recorded_backing_track.file_offset = 0
recorded_backing_track[:url] = construct_filename(recording.created_at, recording.id, backing_track.client_track_id)
recorded_backing_track.save
recorded_backing_track
end
def sign_url(expiration_time = 120)
s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false})
end
def can_download?(some_user)
claimed_recording = recording.claimed_recordings.find{|claimed_recording| claimed_recording.user == some_user }
if claimed_recording
!claimed_recording.discarded
else
false
end
end
def too_many_upload_failures?
upload_failures >= APP_CONFIG.max_track_upload_failures
end
def too_many_downloads?
(self.download_count < 0 || self.download_count > APP_CONFIG.max_audio_downloads) && !@current_user.admin
end
def upload_starting?
next_part_to_upload_was == 0 && next_part_to_upload == 1
end
def validate_too_many_upload_failures
if upload_failures >= APP_CONFIG.max_track_upload_failures
errors.add(:upload_failures, ValidationMessages::UPLOAD_FAILURES_EXCEEDED)
end
end
def validate_fully_uploaded
if marking_complete && fully_uploaded && fully_uploaded_was
errors.add(:fully_uploaded, ValidationMessages::ALREADY_UPLOADED)
end
end
def validate_part_complete
# if we see a transition from is_part_uploading from true to false, we validate
if is_part_uploading_was && !is_part_uploading
if next_part_to_upload_was + 1 != next_part_to_upload
errors.add(:next_part_to_upload, ValidationMessages::INVALID_PART_NUMBER_SPECIFIED)
end
if file_offset > length
errors.add(:file_offset, ValidationMessages::FILE_OFFSET_EXCEEDS_LENGTH)
end
elsif next_part_to_upload_was + 1 == next_part_to_upload
# this makes sure we are only catching 'upload_part_complete' transitions, and not upload_start
if next_part_to_upload_was != 0
# we see that the part number was ticked--but was is_part_upload set to true before this transition?
if !is_part_uploading_was && !is_part_uploading
errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_STARTED)
end
end
end
end
def verify_download_count
if (self.download_count < 0 || self.download_count > APP_CONFIG.max_audio_downloads) && !@current_user.admin
errors.add(:download_count, "must be less than or equal to 100")
end
end
def upload_start(length, md5)
#self.upload_id set by the observer
self.next_part_to_upload = 1
self.length = length
self.md5 = md5
save
end
# if for some reason the server thinks the client can't carry on with the upload,
# this resets everything to the initial state
def reset_upload
self.upload_failures = self.upload_failures + 1
self.part_failures = 0
self.file_offset = 0
self.next_part_to_upload = 0
self.upload_id = nil
self.md5 = nil
self.length = 0
self.fully_uploaded = false
self.is_part_uploading = false
save :validate => false # skip validation because we need this to always work
end
def upload_next_part(length, md5)
self.marking_complete = true
if next_part_to_upload == 0
upload_start(length, md5)
end
self.is_part_uploading = true
save
end
def upload_sign(content_md5)
s3_manager.upload_sign(self[:url], content_md5, next_part_to_upload, upload_id)
end
def upload_part_complete(part, offset)
# validated by :validate_part_complete
self.marking_complete = true
self.is_part_uploading = false
self.next_part_to_upload = self.next_part_to_upload + 1
self.file_offset = offset.to_i
self.part_failures = 0
save
end
def upload_complete
# validate from happening twice by :validate_fully_uploaded
self.fully_uploaded = true
self.marking_complete = true
save
end
def increment_part_failures(part_failure_before_error)
self.part_failures = part_failure_before_error + 1
RecordedBackingTrack.update_all("part_failures = #{self.part_failures}", "id = '#{self.id}'")
end
def stored_filename
# construct a path from s3
RecordedBacknigTrack.construct_filename(recording.created_at, self.recording.id, self.client_track_id)
end
def update_download_count(count=1)
self.download_count = self.download_count + count
self.last_downloaded_at = Time.now
end
def delete_s3_files
s3_manager.delete(self[:url]) if self[:url] && s3_manager.exists?(self[:url])
end
def mark_silent
destroy
# check if we have all the files we need, now that the recorded_backing_track is out of the way
recording.preconditions_for_mix?
end
private
def self.construct_filename(created_at, recording_id, client_track_id)
raise "unknown ID" unless client_track_id
"recordings/#{created_at.strftime('%m-%d-%Y')}/#{recording_id}/backing-track-#{client_track_id}.ogg"
end
end
end

View File

@ -0,0 +1,91 @@
module JamRuby
class RecordedBackingTrackObserver < ActiveRecord::Observer
# if you change the this class, tests really should accompany. having alot of logic in observers is really tricky, as we do here
observe JamRuby::RecordedBackingTrack
def before_validation(recorded_backing_tracks)
# if we see that a part was just uploaded entirely, validate that we can find the part that was just uploaded
if recorded_backing_tracks.is_part_uploading_was && !recorded_backing_tracks.is_part_uploading
begin
aws_part = recorded_backing_tracks.s3_manager.multiple_upload_find_part(recorded_backing_tracks[:url], recorded_backing_tracks.upload_id, recorded_backing_tracks.next_part_to_upload - 1)
# calling size on a part that does not exist will throw an exception... that's what we want
aws_part.size
rescue SocketError => e
raise # this should cause a 500 error, which is what we want. The client will retry later on 500.
rescue Exception => e
recorded_backing_tracks.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS)
rescue RuntimeError => e
recorded_backing_tracks.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS)
rescue
recorded_backing_tracks.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS)
end
end
# if we detect that this just became fully uploaded -- if so, tell s3 to put the parts together
if recorded_backing_tracks.marking_complete && !recorded_backing_tracks.fully_uploaded_was && recorded_backing_tracks.fully_uploaded
multipart_success = false
begin
recorded_backing_tracks.s3_manager.multipart_upload_complete(recorded_backing_tracks[:url], recorded_backing_tracks.upload_id)
multipart_success = true
rescue SocketError => e
raise # this should cause a 500 error, which is what we want. The client will retry later.
rescue Exception => e
#recorded_track.reload
recorded_backing_tracks.reset_upload
recorded_backing_tracks.errors.add(:upload_id, ValidationMessages::BAD_UPLOAD)
end
# unlike RecordedTracks, only the person who uploaded can download it, so no need to notify
# tell all users that a download is available, except for the user who just uploaded
# recorded_backing_tracks.recording.users.each do |user|
#Notification.send_download_available(recorded_backing_tracks.user_id) unless user == recorded_backing_tracks.user
# end
end
end
def after_commit(recorded_backing_track)
end
# here we tick upload failure counts, or revert the state of the model, as needed
def after_rollback(recorded_backing_track)
# if fully uploaded, don't increment failures
if recorded_backing_track.fully_uploaded
return
end
# increment part failures if there is a part currently being uploaded
if recorded_backing_track.is_part_uploading_was
#recorded_track.reload # we don't want anything else that the user set to get applied
recorded_backing_track.increment_part_failures(recorded_backing_track.part_failures_was)
if recorded_backing_track.part_failures >= APP_CONFIG.max_track_part_upload_failures
# save upload id before we abort this bad boy
upload_id = recorded_backing_track.upload_id
begin
recorded_backing_track.s3_manager.multipart_upload_abort(recorded_backing_track[:url], upload_id)
rescue => e
puts e.inspect
end
recorded_backing_track.reset_upload
if recorded_backing_track.upload_failures >= APP_CONFIG.max_track_upload_failures
# do anything?
end
end
end
end
def before_save(recorded_backing_track)
# if we are on the 1st part, then we need to make sure we can save the upload_id
if recorded_backing_track.next_part_to_upload == 1
recorded_backing_track.upload_id = recorded_backing_track.s3_manager.multipart_upload_start(recorded_backing_track[:url])
end
end
end
end

View File

@ -11,6 +11,7 @@ module JamRuby
has_many :quick_mixes, :class_name => "JamRuby::QuickMix", :foreign_key => :recording_id, :dependent => :destroy
has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id, :dependent => :destroy
has_many :recorded_videos, :class_name => "JamRuby::RecordedVideo", :foreign_key => :recording_id, :dependent => :destroy
has_many :recorded_backing_tracks, :class_name => "JamRuby::RecordedBackingTrack", :foreign_key => :recording_id, :dependent => :destroy
has_many :comments, :class_name => "JamRuby::RecordingComment", :foreign_key => "recording_id", :dependent => :destroy
has_many :likes, :class_name => "JamRuby::RecordingLiker", :foreign_key => "recording_id", :dependent => :destroy
has_many :plays, :class_name => "JamRuby::PlayablePlay", :as => :playable, :dependent => :destroy
@ -179,6 +180,14 @@ module JamRuby
recorded_tracks.where(:user_id => user.id)
end
def recorded_backing_tracks_for_user(user)
unless self.users.exists?(user)
raise PermissionError, "user was not in this session"
end
recorded_backing_tracks.where(:user_id => user.id)
end
def has_access?(user)
users.exists?(user)
end
@ -209,6 +218,10 @@ module JamRuby
connection.video_sources.each do |video|
recording.recorded_videos << RecordedVideo.create_from_video_source(video, recording)
end
connection.backing_tracks.each do |backing_track|
recording.recorded_backing_tracks << RecordedBackingTrack.create_from_backing_track(backing_track, recording)
end
end
end
end
@ -321,8 +334,7 @@ module JamRuby
}
)
end
latest_recorded_track = downloads[-1][:next] if downloads.length > 0
latest_recorded_track = (downloads.length > 0) ? downloads[-1][:next] : 0
Mix.joins(:recording).joins(:recording => :claimed_recordings)
.order('mixes.id')
@ -345,16 +357,31 @@ module JamRuby
}
)
end
latest_mix = (downloads.length > 0) ? downloads[-1][:next] : 0
latest_mix = downloads[-1][:next] if downloads.length > 0
if !latest_mix.nil? && !latest_recorded_track.nil?
next_date = [latest_mix, latest_recorded_track].max
elsif latest_mix.nil?
next_date = latest_recorded_track
else
next_date = latest_mix
RecordedBackingTrack.joins(:recording).joins(:recording => :claimed_recordings)
.order('recorded_backing_tracks.id')
.where('recorded_backing_tracks.fully_uploaded = TRUE')
.where('recorded_backing_tracks.id > ?', since)
.where('recorded_backing_tracks.user_id = ?', user.id) # only the person who opened the backing track can have it back
.where('all_discarded = false')
.where('deleted = false')
.where('claimed_recordings.user_id = ? AND claimed_recordings.discarded = FALSE', user).limit(limit).each do |recorded_backing_track|
downloads.push(
{
:type => "recorded_backing_track",
:id => recorded_backing_track.client_track_id,
:recording_id => recorded_backing_track.recording_id,
:length => recorded_backing_track.length,
:md5 => recorded_backing_track.md5,
:url => recorded_backing_track[:url],
:next => recorded_backing_track.id
}
)
end
latest_recorded_backing_track = (downloads.length > 0) ? downloads[-1][:next] : 0
next_date = [latest_mix, latest_recorded_track, latest_recorded_backing_track].max
if next_date.nil?
next_date = since # echo back to the client the same value they passed in, if there are no results
@ -417,6 +444,20 @@ module JamRuby
Arel::Nodes::As.new('stream_mix', Arel.sql('item_type'))
]).reorder("")
# Select fields for quick mix. Note that it must include
# the same number of fields as the track or video in order for
# the union to work:
backing_track_arel = RecordedBackingTrack.select([
:id,
:recording_id,
:user_id,
:url,
:fully_uploaded,
:upload_failures,
:client_track_id,
Arel::Nodes::As.new('backing_track', Arel.sql('item_type'))
]).reorder("")
# Glue them together:
union = track_arel.union(vid_arel)
@ -439,7 +480,25 @@ module JamRuby
])
# And repeat:
union_all = arel.union(quick_mix_arel)
union_quick = arel.union(quick_mix_arel)
utable_quick = Arel::Nodes::TableAlias.new(union_quick, :recorded_items_quick)
arel = arel.from(utable_quick)
arel = arel.except(:select)
arel = arel.select([
"recorded_items_quick.id",
:recording_id,
:user_id,
:url,
:fully_uploaded,
:upload_failures,
:client_track_id,
:item_type
])
# And repeat for backing track:
union_all = arel.union(backing_track_arel)
utable_all = Arel::Nodes::TableAlias.new(union_all, :recorded_items_all)
arel = arel.from(utable_all)
@ -455,7 +514,6 @@ module JamRuby
:item_type
])
# Further joining and criteria for the unioned object:
arel = arel.joins("INNER JOIN recordings ON recordings.id=recorded_items_all.recording_id") \
.where('recorded_items_all.user_id' => user.id) \
@ -492,6 +550,13 @@ module JamRuby
:recording_id => recorded_item.recording_id,
:next => recorded_item.id
})
elsif recorded_item.item_type == 'backing_track'
uploads << ({
:type => "recorded_backing_track",
:recording_id => recorded_item.recording_id,
:client_track_id => recorded_item.client_track_id,
:next => recorded_item.id
})
else
end
@ -513,6 +578,11 @@ module JamRuby
recorded_tracks.each do |recorded_track|
return false unless recorded_track.fully_uploaded
end
recorded_backing_tracks.each do |recorded_backing_track|
return false unless recorded_backing_track.fully_uploaded
end
true
end

View File

@ -55,11 +55,64 @@ module JamRuby
return query
end
def self.diff_track(track_class, existing_tracks, new_tracks, &blk)
result = []
if new_tracks.length == 0
existing_tracks.delete_all
else
# we will prune from this as we find matching tracks
to_delete = Set.new(existing_tracks)
to_add = Array.new(new_tracks)
existing_tracks.each do |existing_track|
new_tracks.each do |new_track|
if new_track[:id] == existing_track.id || new_track[:client_track_id] == existing_track.client_track_id
to_delete.delete(existing_track)
to_add.delete(new_track)
blk.call(existing_track, new_track)
result.push(existing_track)
if existing_track.save
next
else
result = existing_track
raise ActiveRecord::Rollback
end
end
end
end
to_add.each do |new_track|
existing_track = track_class.new
blk.call(existing_track, new_track)
if existing_track.save
result.push(existing_track)
else
result = existing_track
raise ActiveRecord::Rollback
end
end
to_delete.each do |delete_me|
delete_me.delete
end
end
result
end
# this is a bit different from a normal track synchronization in that the client just sends up all tracks,
# ... some may already exist
def self.sync(clientId, tracks)
result = []
def self.sync(clientId, tracks, backing_tracks = [])
result = {}
backing_tracks = [] unless backing_tracks
Track.transaction do
connection = Connection.find_by_client_id!(clientId)
@ -68,67 +121,28 @@ module JamRuby
msh = MusicSessionUserHistory.find_by_client_id!(clientId)
instruments = []
if tracks.length == 0
connection.tracks.delete_all
else
connection_tracks = connection.tracks
tracks.each do |track|
instruments << track[:instrument_id]
end
# we will prune from this as we find matching tracks
to_delete = Set.new(connection_tracks)
to_add = Array.new(tracks)
result[:tracks] = diff_track(Track, connection.tracks, tracks) do |track_record, track_info|
track_record.connection = connection
track_record.client_track_id = track_info[:client_track_id]
track_record.client_resource_id = track_info[:client_resource_id]
track_record.instrument_id = track_info[:instrument_id]
track_record.sound = track_info[:sound]
end
tracks.each do |track|
instruments << track[:instrument_id]
end
result[:backing_tracks] = diff_track(BackingTrack, connection.backing_tracks, backing_tracks) do |track_record, track_info|
track_record.connection = connection
track_record.client_track_id = track_info[:client_track_id]
track_record.client_resource_id = track_info[:client_resource_id]
track_record.filename = track_info[:filename]
end
connection_tracks.each do |connection_track|
tracks.each do |track|
if track[:id] == connection_track.id || track[:client_track_id] == connection_track.client_track_id
to_delete.delete(connection_track)
to_add.delete(track)
# don't update connection_id or client_id; it's unknown what would happen if these changed mid-session
connection_track.instrument_id = track[:instrument_id]
connection_track.sound = track[:sound]
connection_track.client_track_id = track[:client_track_id]
connection_track.client_resource_id = track[:client_resource_id]
result.push(connection_track)
if connection_track.save
next
else
result = connection_track
raise ActiveRecord::Rollback
end
end
end
end
msh.instruments = instruments.join("|")
if !msh.save
raise ActiveRecord::Rollback
end
to_add.each do |track|
connection_track = Track.new
connection_track.connection = connection
connection_track.instrument_id = track[:instrument_id]
connection_track.sound = track[:sound]
connection_track.client_track_id = track[:client_track_id]
connection_track.client_resource_id = track[:client_resource_id]
if connection_track.save
result.push(connection_track)
else
result = connection_track
raise ActiveRecord::Rollback
end
end
to_delete.each do |delete_me|
delete_me.delete
end
msh.instruments = instruments.join("|")
if !msh.save
raise ActiveRecord::Rollback
end
end

View File

@ -122,6 +122,7 @@ module JamRuby
# saved tracks
has_many :recorded_tracks, :foreign_key => "user_id", :class_name => "JamRuby::RecordedTrack", :inverse_of => :user
has_many :recorded_videos, :foreign_key => "user_id", :class_name => "JamRuby::RecordedVideo", :inverse_of => :user
has_many :recorded_backing_tracks, :foreign_key => "user_id", :class_name => "JamRuby::RecordedBackingTrack", :inverse_of => :user
has_many :quick_mixes, :foreign_key => "user_id", :class_name => "JamRuby::QuickMix", :inverse_of => :user
# invited users

View File

@ -4,6 +4,7 @@ module JamRuby
belongs_to :recorded_track
belongs_to :mix
belongs_to :quick_mix
belongs_to :recorded_backing_track
def self.show(id, user_id)
self.index({user_id: user_id, id: id, limit: 1, offset: 0})[:query].first
@ -22,7 +23,7 @@ module JamRuby
raise 'no user id specified' if user_id.blank?
query = UserSync
.includes(recorded_track: [{recording: [:owner, {claimed_recordings: [:share_token]}, {recorded_tracks: [:user]}, {comments:[:user]}, :likes, :plays, :mixes]}, user: [], instrument:[]], mix: [], quick_mix:[])
.includes(recorded_track: [{recording: [:owner, {claimed_recordings: [:share_token]}, {recorded_tracks: [:user]}, {comments:[:user]}, :likes, :plays, :mixes]}, user: [], instrument:[]], mix: [], quick_mix:[], recorded_backing_track:[])
.joins("LEFT OUTER JOIN claimed_recordings ON claimed_recordings.user_id = user_syncs.user_id AND claimed_recordings.recording_id = user_syncs.recording_id")
.where(user_id: user_id)
.where(%Q{

View File

@ -232,6 +232,11 @@ FactoryGirl.define do
sequence(:client_resource_id) { |n| "resource_id#{n}"}
end
factory :backing_track, :class => JamRuby::BackingTrack do
sequence(:client_track_id) { |n| "client_track_id#{n}"}
filename 'foo.mp3'
end
factory :video_source, :class => JamRuby::VideoSource do
#client_video_source_id "test_source_id"
sequence(:client_video_source_id) { |n| "client_video_source_id#{n}"}
@ -250,6 +255,20 @@ FactoryGirl.define do
association :recording, factory: :recording
end
factory :recorded_backing_track, :class => JamRuby::RecordedBackingTrack do
sequence(:client_id) { |n| "client_id-#{n}"}
sequence(:backing_track_id) { |n| "track_id-#{n}"}
sequence(:client_track_id) { |n| "client_track_id-#{n}"}
sequence(:filename) { |n| "filename-{#n}"}
sequence(:url) { |n| "/recordings/blah/#{n}"}
md5 'abc'
length 1
fully_uploaded true
association :user, factory: :user
association :recording, factory: :recording
end
factory :recorded_video, :class => JamRuby::RecordedVideo do
sequence(:client_video_source_id) { |n| "client_video_source_id-#{n}"}
fully_uploaded true

View File

@ -745,6 +745,29 @@ describe ActiveMusicSession do
@music_session.errors[:claimed_recording] == [ValidationMessages::JAM_TRACK_ALREADY_OPEN]
end
it "disallow a claimed recording to be started when backing track is open" do
# open the backing track
@backing_track = "foo.mp3"
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_false
# and try to open a recording for playback
@music_session.claimed_recording_start(@user1, @claimed_recording)
@music_session.errors.any?.should be_true
@music_session.errors[:claimed_recording] == [ValidationMessages::BACKING_TRACK_ALREADY_OPEN]
end
it "disallow a claimed recording to be started when metronome is open" do
# open the metronome
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_false
# and try to open a recording for playback
@music_session.claimed_recording_start(@user1, @claimed_recording)
@music_session.errors.any?.should be_true
@music_session.errors[:claimed_recording] == [ValidationMessages::METRONOME_ALREADY_OPEN]
end
end
end
@ -830,5 +853,143 @@ describe ActiveMusicSession do
music_sessions[0].connections[0].tracks.should have(1).items
end
end
describe "open_backing_track" do
before(:each) do
@user1 = FactoryGirl.create(:user)
@connection = FactoryGirl.create(:connection, :user => @user1)
@instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
@track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
@music_session = FactoryGirl.create(:active_music_session, :creator => @user1, :musician_access => true)
# @music_session.connections << @connection
@music_session.save!
@connection.join_the_session(@music_session, true, nil, @user1, 10)
@backing_track = "foo/bar.mp3"
end
it "allow a backing track to be associated" do
# simple success case; just open the backing track and observe the state of the session is correct
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_false
@music_session.reload
@music_session.backing_track_path.should == @backing_track
@music_session.backing_track_initiator.should == @user1
end
it "allow a backing track to be closed" do
# simple success case; close an opened backing track and observe the state of the session is correct
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_false
@music_session.close_backing_track
@music_session.errors.any?.should be_false
@music_session.reload
@music_session.backing_track_path.should be_nil
@music_session.backing_track_initiator.should be_nil
end
it "disallow a backing track to be opened when another is already opened" do
# if a backing track is open, don't allow another to be opened
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_false
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_true
@music_session.errors[:backing_track] == [ValidationMessages::BACKING_TRACK_ALREADY_OPEN]
end
it "disallow a backing track to be opened when recording is ongoing" do
@recording = Recording.start(@music_session, @user1)
@music_session.errors.any?.should be_false
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_true
@music_session.errors[:backing_track] == [ValidationMessages::RECORDING_ALREADY_IN_PROGRESS]
end
it "disallow a backing track to be opened when recording is playing back" do
# create a recording, and open it for play back
@recording = Recording.start(@music_session, @user1)
@recording.errors.any?.should be_false
@recording.stop
@recording.reload
@claimed_recording = @recording.claim(@user1, "name", "description", Genre.first, true)
@claimed_recording.errors.any?.should be_false
@music_session.claimed_recording_start(@user1, @claimed_recording)
@music_session.errors.any?.should be_false
# while it's open, try to open a jam track
@music_session.open_backing_track(@user1, @backing_track)
@music_session.errors.any?.should be_true
@music_session.errors[:backing_track] == [ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS]
end
end
describe "open_metronome" do
before(:each) do
@user1 = FactoryGirl.create(:user)
@connection = FactoryGirl.create(:connection, :user => @user1)
@instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
@track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
@music_session = FactoryGirl.create(:active_music_session, :creator => @user1, :musician_access => true)
# @music_session.connections << @connection
@music_session.save!
@connection.join_the_session(@music_session, true, nil, @user1, 10)
end
it "allow a metronome to be activated" do
# simple success case; just open the metronome and observe the state of the session is correct
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_false
@music_session.reload
@music_session.metronome_active.should == true
@music_session.metronome_initiator.should == @user1
end
it "allow a metronome to be closed" do
# simple success case; close an opened metronome and observe the state of the session is correct
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_false
@music_session.close_metronome
@music_session.errors.any?.should be_false
@music_session.reload
@music_session.metronome_active.should be_false
@music_session.metronome_initiator.should be_nil
end
it "disallow a metronome to be opened when another is already opened" do
# if a metronome is open, don't allow another to be opened
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_false
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_true
@music_session.errors[:metronome] == [ValidationMessages::METRONOME_ALREADY_OPEN]
end
it "disallow a metronome to be opened when recording is ongoing" do
@recording = Recording.start(@music_session, @user1)
@music_session.errors.any?.should be_false
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_true
@music_session.errors[:metronome] == [ValidationMessages::RECORDING_ALREADY_IN_PROGRESS]
end
it "disallow a metronome to be opened when recording is playing back" do
# create a recording, and open it for play back
@recording = Recording.start(@music_session, @user1)
@recording.errors.any?.should be_false
@recording.stop
@recording.reload
@claimed_recording = @recording.claim(@user1, "name", "description", Genre.first, true)
@claimed_recording.errors.any?.should be_false
@music_session.claimed_recording_start(@user1, @claimed_recording)
@music_session.errors.any?.should be_false
# while it's open, try to open a jam track
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_true
@music_session.errors[:metronome] == [ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS]
end
end
end

View File

@ -0,0 +1,228 @@
require 'spec_helper'
require 'rest-client'
describe RecordedBackingTrack do
include UsesTempFiles
before do
@user = FactoryGirl.create(:user)
@connection = FactoryGirl.create(:connection, :user => @user)
@instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
@music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true)
@track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
@backing_track = FactoryGirl.create(:backing_track, :connection => @connection)
@recording = FactoryGirl.create(:recording, :music_session => @music_session, :owner => @user)
end
it "should copy from a regular track properly" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.user.id.should == @backing_track.connection.user.id
@recorded_backing_track.filename.should == @backing_track.filename
@recorded_backing_track.next_part_to_upload.should == 0
@recorded_backing_track.fully_uploaded.should == false
@recorded_backing_track.client_id = @connection.client_id
@recorded_backing_track.backing_track_id = @backing_track.id
end
it "should update the next part to upload properly" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.upload_part_complete(1, 1000)
@recorded_backing_track.errors.any?.should be_true
@recorded_backing_track.errors[:length][0].should == "is too short (minimum is 1 characters)"
@recorded_backing_track.errors[:md5][0].should == "can't be blank"
end
it "properly finds a recorded track given its upload filename" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.save.should be_true
RecordedBackingTrack.find_by_recording_id_and_backing_track_id(@recorded_backing_track.recording_id, @recorded_backing_track.backing_track_id).should == @recorded_backing_track
end
it "gets a url for the track" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.errors.any?.should be_false
@recorded_backing_track[:url].should == "recordings/#{@recorded_backing_track.created_at.strftime('%m-%d-%Y')}/#{@recording.id}/backing-track-#{@backing_track.client_track_id}.ogg"
end
it "signs url" do
stub_const("APP_CONFIG", app_config)
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.sign_url.should_not be_nil
end
it "can not be downloaded if no claimed recording" do
user2 = FactoryGirl.create(:user)
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.can_download?(user2).should be_false
@recorded_backing_track.can_download?(@user).should be_false
end
it "can be downloaded if there is a claimed recording" do
@recorded_track = RecordedTrack.create_from_track(@track, @recording)
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recording.claim(@user, "my recording", "my description", Genre.first, true).errors.any?.should be_false
@recorded_backing_track.can_download?(@user).should be_true
end
describe "aws-based operations", :aws => true do
def put_file_to_aws(signed_data, contents)
begin
RestClient.put( signed_data[:url],
contents,
{
:'Content-Type' => 'audio/ogg',
:Date => signed_data[:datetime],
:'Content-MD5' => signed_data[:md5],
:Authorization => signed_data[:authorization]
})
rescue => e
puts e.response
raise e
end
end
# create a test file
upload_file='some_file.ogg'
in_directory_with_file(upload_file)
upload_file_contents="ogg binary stuff in here"
md5 = Base64.encode64(Digest::MD5.digest(upload_file_contents)).chomp
test_config = app_config
s3_manager = S3Manager.new(test_config.aws_bucket, test_config.aws_access_key_id, test_config.aws_secret_access_key)
before do
stub_const("APP_CONFIG", app_config)
# this block of code will fully upload a sample file to s3
content_for_file(upload_file_contents)
s3_manager.delete_folder('recordings') # keep the bucket clean to save cost, and make it easier if post-mortuem debugging
end
it "cant mark a part complete without having started it" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.upload_start(1000, "abc")
@recorded_backing_track.upload_part_complete(1, 1000)
@recorded_backing_track.errors.any?.should be_true
@recorded_backing_track.errors[:next_part_to_upload][0].should == ValidationMessages::PART_NOT_STARTED
end
it "no parts" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.upload_start(1000, "abc")
@recorded_backing_track.upload_next_part(1000, "abc")
@recorded_backing_track.errors.any?.should be_false
@recorded_backing_track.upload_part_complete(1, 1000)
@recorded_backing_track.errors.any?.should be_true
@recorded_backing_track.errors[:next_part_to_upload][0].should == ValidationMessages::PART_NOT_FOUND_IN_AWS
end
it "enough part failures reset the upload" do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.upload_start(File.size(upload_file), md5)
@recorded_backing_track.upload_next_part(File.size(upload_file), md5)
@recorded_backing_track.errors.any?.should be_false
APP_CONFIG.max_track_part_upload_failures.times do |i|
@recorded_backing_track.upload_part_complete(@recorded_backing_track.next_part_to_upload, File.size(upload_file))
@recorded_backing_track.errors[:next_part_to_upload] == [ValidationMessages::PART_NOT_FOUND_IN_AWS]
part_failure_rollover = i == APP_CONFIG.max_track_part_upload_failures - 1
expected_is_part_uploading = !part_failure_rollover
expected_part_failures = part_failure_rollover ? 0 : i + 1
@recorded_backing_track.reload
@recorded_backing_track.is_part_uploading.should == expected_is_part_uploading
@recorded_backing_track.part_failures.should == expected_part_failures
end
@recorded_backing_track.reload
@recorded_backing_track.upload_failures.should == 1
@recorded_backing_track.file_offset.should == 0
@recorded_backing_track.next_part_to_upload.should == 0
@recorded_backing_track.upload_id.should be_nil
@recorded_backing_track.md5.should be_nil
@recorded_backing_track.length.should == 0
end
it "enough upload failures fails the upload forever" do
APP_CONFIG.stub(:max_track_upload_failures).and_return(1)
APP_CONFIG.stub(:max_track_part_upload_failures).and_return(2)
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
APP_CONFIG.max_track_upload_failures.times do |j|
@recorded_backing_track.upload_start(File.size(upload_file), md5)
@recorded_backing_track.upload_next_part(File.size(upload_file), md5)
@recorded_backing_track.errors.any?.should be_false
APP_CONFIG.max_track_part_upload_failures.times do |i|
@recorded_backing_track.upload_part_complete(@recorded_backing_track.next_part_to_upload, File.size(upload_file))
@recorded_backing_track.errors[:next_part_to_upload] == [ValidationMessages::PART_NOT_FOUND_IN_AWS]
part_failure_rollover = i == APP_CONFIG.max_track_part_upload_failures - 1
expected_is_part_uploading = part_failure_rollover ? false : true
expected_part_failures = part_failure_rollover ? 0 : i + 1
@recorded_backing_track.reload
@recorded_backing_track.is_part_uploading.should == expected_is_part_uploading
@recorded_backing_track.part_failures.should == expected_part_failures
end
@recorded_backing_track.upload_failures.should == j + 1
end
@recorded_backing_track.reload
@recorded_backing_track.upload_failures.should == APP_CONFIG.max_track_upload_failures
@recorded_backing_track.file_offset.should == 0
@recorded_backing_track.next_part_to_upload.should == 0
@recorded_backing_track.upload_id.should be_nil
@recorded_backing_track.md5.should be_nil
@recorded_backing_track.length.should == 0
# try to poke it and get the right kind of error back
@recorded_backing_track.upload_next_part(File.size(upload_file), md5)
@recorded_backing_track.errors[:upload_failures] = [ValidationMessages::UPLOAD_FAILURES_EXCEEDED]
end
describe "correctly uploaded a file" do
before do
@recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording)
@recorded_backing_track.upload_start(File.size(upload_file), md5)
@recorded_backing_track.upload_next_part(File.size(upload_file), md5)
signed_data = @recorded_backing_track.upload_sign(md5)
@response = put_file_to_aws(signed_data, upload_file_contents)
@recorded_backing_track.upload_part_complete(@recorded_backing_track.next_part_to_upload, File.size(upload_file))
@recorded_backing_track.errors.any?.should be_false
@recorded_backing_track.upload_complete
@recorded_backing_track.errors.any?.should be_false
@recorded_backing_track.marking_complete = false
end
it "can download an updated file" do
@response = RestClient.get @recorded_backing_track.sign_url
@response.body.should == upload_file_contents
end
it "can't mark completely uploaded twice" do
@recorded_backing_track.upload_complete
@recorded_backing_track.errors.any?.should be_true
@recorded_backing_track.errors[:fully_uploaded][0].should == "already set"
@recorded_backing_track.part_failures.should == 0
end
it "can't ask for a next part if fully uploaded" do
@recorded_backing_track.upload_next_part(File.size(upload_file), md5)
@recorded_backing_track.errors.any?.should be_true
@recorded_backing_track.errors[:fully_uploaded][0].should == "already set"
@recorded_backing_track.part_failures.should == 0
end
it "can't ask for mark part complete if fully uploaded" do
@recorded_backing_track.upload_part_complete(1, 1000)
@recorded_backing_track.errors.any?.should be_true
@recorded_backing_track.errors[:fully_uploaded][0].should == "already set"
@recorded_backing_track.part_failures.should == 0
end
end
end
end

View File

@ -211,6 +211,20 @@ describe Recording do
user1_recorded_tracks[0].discard = true
user1_recorded_tracks[0].save!
end
it "should allow finding of backing tracks" do
user2 = FactoryGirl.create(:user)
connection2 = FactoryGirl.create(:connection, :user => user2, :music_session => @music_session)
track2 = FactoryGirl.create(:track, :connection => connection2, :instrument => @instrument)
backing_track = FactoryGirl.create(:backing_track, :connection => connection2)
@recording = Recording.start(@music_session, @user)
@recording.recorded_backing_tracks_for_user(@user).length.should eq(0)
user2_recorded_tracks = @recording.recorded_backing_tracks_for_user(user2)
user2_recorded_tracks.length.should == 1
user2_recorded_tracks[0].should == user2.recorded_backing_tracks[0]
end
it "should set up the recording properly when recording is started with 1 user in the session" do
@music_session.is_recording?.should be_false
@ -547,6 +561,8 @@ describe Recording do
@genre = FactoryGirl.create(:genre)
@recording.claim(@user, "Recording", "Recording Description", @genre, true)
@backing_track = FactoryGirl.create(:backing_track, :connection => @connection)
# We should have 2 items; a track and a video:
uploads = Recording.list_uploads(@user)
uploads["uploads"].should have(3).items

View File

@ -7,8 +7,10 @@ describe Track do
let (:connection) { FactoryGirl.create(:connection, :user => user, :music_session => music_session) }
let (:track) { FactoryGirl.create(:track, :connection => connection)}
let (:track2) { FactoryGirl.create(:track, :connection => connection)}
let (:backing_track) { FactoryGirl.create(:backing_track, :connection => connection)}
let (:msuh) {FactoryGirl.create(:music_session_user_history, :history => music_session.music_session, :user => user, :client_id => connection.client_id) }
let (:track_hash) { {:client_track_id => 'client_guid', :sound => 'stereo', :instrument_id => 'drums'} }
let (:backing_track_hash) { {:client_track_id => 'client_guid', :filename => "blah.wav"} }
before(:each) do
msuh.touch
@ -16,7 +18,8 @@ describe Track do
describe "sync" do
it "create one track" do
tracks = Track.sync(connection.client_id, [track_hash])
result = Track.sync(connection.client_id, [track_hash])
tracks = result[:tracks]
tracks.length.should == 1
track = tracks[0]
track.client_track_id.should == track_hash[:client_track_id]
@ -25,7 +28,8 @@ describe Track do
end
it "create two tracks" do
tracks = Track.sync(connection.client_id, [track_hash, track_hash])
result = Track.sync(connection.client_id, [track_hash, track_hash])
tracks = result[:tracks]
tracks.length.should == 2
track = tracks[0]
track.client_track_id.should == track_hash[:client_track_id]
@ -40,7 +44,8 @@ describe Track do
it "delete only track" do
track.id.should_not be_nil
connection.tracks.length.should == 1
tracks = Track.sync(connection.client_id, [])
result = Track.sync(connection.client_id, [])
tracks = result[:tracks]
tracks.length.should == 0
end
@ -49,7 +54,8 @@ describe Track do
track.id.should_not be_nil
track2.id.should_not be_nil
connection.tracks.length.should == 2
tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
result = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
@ -62,7 +68,8 @@ describe Track do
track.id.should_not be_nil
track2.id.should_not be_nil
connection.tracks.length.should == 2
tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
result = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
@ -75,7 +82,8 @@ describe Track do
track.id.should_not be_nil
connection.tracks.length.should == 1
set_updated_at(track, 1.days.ago)
tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
result = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
@ -87,7 +95,8 @@ describe Track do
it "updates a single track using .client_track_id to correlate" do
track.id.should_not be_nil
connection.tracks.length.should == 1
tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
result = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
@ -99,11 +108,69 @@ describe Track do
track.id.should_not be_nil
connection.tracks.length.should == 1
set_updated_at(track, 1.days.ago)
tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}])
result = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
expect(found.id).to eq track.id
expect(found.updated_at.to_i).to eq track.updated_at.to_i
end
describe "backing tracks" do
it "create one track and one backing track" do
result = Track.sync(connection.client_id, [track_hash], [backing_track_hash])
tracks = result[:tracks]
tracks.length.should == 1
track = tracks[0]
track.client_track_id.should == track_hash[:client_track_id]
track.sound = track_hash[:sound]
track.instrument.should == Instrument.find('drums')
backing_tracks = result[:backing_tracks]
backing_tracks.length.should == 1
track = backing_tracks[0]
track.client_track_id.should == backing_track_hash[:client_track_id]
end
it "delete only backing_track" do
track.id.should_not be_nil
backing_track.id.should_not be_nil
connection.tracks.length.should == 1
connection.backing_tracks.length.should == 1
result = Track.sync(connection.client_id,
[{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}],
[])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
expect(found.id).to eq track.id
expect(found.updated_at.to_i).to eq track.updated_at.to_i
backing_tracks = result[:backing_tracks]
backing_tracks.length.should == 0
end
it "does not touch updated_at when nothing changes" do
track.id.should_not be_nil
backing_track.id.should_not be_nil
connection.tracks.length.should == 1
set_updated_at(track, 1.days.ago)
set_updated_at(backing_track, 1.days.ago)
result = Track.sync(connection.client_id,
[{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}],
[{:id => backing_track.id, :client_track_id => backing_track.client_track_id, :filename => backing_track.filename, client_resource_id: backing_track.client_resource_id}])
tracks = result[:tracks]
tracks.length.should == 1
found = tracks[0]
expect(found.id).to eq track.id
expect(found.updated_at.to_i).to eq track.updated_at.to_i
backing_tracks = result[:backing_tracks]
backing_tracks.length.should == 1
found = backing_tracks[0]
expect(found.id).to eq backing_track.id
expect(found.updated_at.to_i).to eq backing_track.updated_at.to_i
end
end
end
end

View File

@ -20,6 +20,49 @@ describe UserSync do
data[:next].should be_nil
end
describe "backing_tracks" do
let!(:recording1) {
recording = FactoryGirl.create(:recording, owner: user1, band: nil, duration:1)
recording.recorded_tracks << FactoryGirl.create(:recorded_track, recording: recording, user: recording.owner, fully_uploaded:false)
recording.recorded_tracks << FactoryGirl.create(:recorded_track, recording: recording, user: user2, fully_uploaded:false)
recording.recorded_backing_tracks << FactoryGirl.create(:recorded_backing_track, recording: recording, user: recording.owner, fully_uploaded:false)
recording.save!
recording.reload
recording
}
let(:sorted_tracks) {
Array.new(recording1.recorded_tracks).sort! {|a, b|
if a.created_at == b.created_at
a.id <=> b.id
else
a.created_at <=> b.created_at
end
}
}
# backing tracks should only list download, or upload, for the person who opened it, for legal reasons
it "lists backing track for opener" do
data = UserSync.index({user_id: user1.id})
data[:next].should be_nil
user_syncs = data[:query]
user_syncs.count.should eq(3)
user_syncs[0].recorded_track.should == sorted_tracks[0]
user_syncs[1].recorded_track.should == sorted_tracks[1]
user_syncs[2].recorded_backing_track.should == recording1.recorded_backing_tracks[0]
end
it "does not list backing track for non-opener" do
data = UserSync.index({user_id: user2.id})
data[:next].should be_nil
user_syncs = data[:query]
user_syncs.count.should eq(2)
user_syncs[0].recorded_track.should == sorted_tracks[0]
user_syncs[1].recorded_track.should == sorted_tracks[1]
end
end
it "one mix and quick mix" do
mix = FactoryGirl.create(:mix)
mix.recording.duration = 1

View File

@ -46,6 +46,7 @@ ActiveRecord::Base.add_observer InvitedUserObserver.instance
ActiveRecord::Base.add_observer UserObserver.instance
ActiveRecord::Base.add_observer FeedbackObserver.instance
ActiveRecord::Base.add_observer RecordedTrackObserver.instance
ActiveRecord::Base.add_observer RecordedBackingTrackObserver.instance
ActiveRecord::Base.add_observer QuickMixObserver.instance
#RecordedTrack.observers.disable :all # only a few tests want this observer active

View File

@ -75,7 +75,7 @@ gem 'netaddr'
gem 'quiet_assets', :group => :development
gem 'bugsnag'
gem 'multi_json', '1.9.0'
gem 'rest_client'
gem 'rest-client'
gem 'iso-639'
gem 'language_list'
gem 'rubyzip'

View File

@ -1,6 +1,3 @@
TODO:
====
Jasmine Javascript Unit Tests
=============================
@ -11,5 +8,3 @@ $ bundle
$ rake jasmine
Open browser to localhost:8888

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -112,6 +112,10 @@
// tell the server we are about to start a recording
rest.startPlayClaimedRecording({id: context.JK.CurrentSessionModel.id(), claimed_recording_id: claimedRecording.id})
.done(function(response) {
// update session info
context.JK.CurrentSessionModel.updateSession(response);
var recordingId = $(this).attr('data-recording-id');
var openRecordingResult = context.jamClient.OpenRecording(claimedRecording.recording);

View File

@ -0,0 +1,147 @@
(function(context,$) {
"use strict";
context.JK = context.JK || {};
context.JK.OpenBackingTrackDialog = function(app) {
var logger = context.JK.logger;
var rest = context.JK.Rest();
var showing = false;
var perPage = 10;
var $dialog = null;
var $tbody = null;
var $paginatorHolder = null;
var $templateOpenBackingTrackRow = null;
var $downloadedTrackHelp = null;
var $whatAreBackingTracks = null;
var $displayAudioFileFolder = null;
function emptyList() {
$tbody.empty();
}
function resetPagination() {
$dialog.find('.paginator').remove();
}
function beforeShow() {
emptyList();
resetPagination();
showing = true;
getBackingTracks();
$dialog.data('result', null);
// .done(function(data, textStatus, jqXHR) {
// // initialize pagination
// var $paginator = context.JK.Paginator.create(parseInt(jqXHR.getResponseHeader('total-entries')), perPage, 0, onPageSelected)
// $paginatorHolder.append($paginator);
// });
}
function afterHide() {
showing = false;
}
function onPageSelected(targetPage) {
return getBackingTracks(targetPage);
}
function getBackingTracks(page) {
var result = context.jamClient.getBackingTrackList();
console.log("result", result)
var backingTracks = result.backing_tracks;
if (!backingTracks || backingTracks.length == 0) {
$tbody.append("<tr><td colspan='100%'>No Tracks found</td></tr>");
} else {
$.each(backingTracks, function(index, backingTrack) {
var extension = backingTrack.name
var options = {
backingTrackState: null,
name: backingTrack.name,
type: getExtension(backingTrack.name),
length: displaySize(backingTrack.size)
}
var $tr = $(context._.template($templateOpenBackingTrackRow.html(), options, { variable: 'data' }));
$tr.data('server-model', backingTrack);
$tbody.append($tr);
});
}//end
}
// from http://stackoverflow.com/questions/190852/how-can-i-get-file-extensions-with-javascript
function getExtension(filename) {
return filename.substr((~-filename.lastIndexOf(".") >>> 0) + 2)
}
// from seth:
function displaySize(length) {
var size = (length==null || typeof(length)=='undefined') ? 0 : Number(length)
return (Math.round(size * 10 / (1024 * 1024) ) / 10).toString() + "M"
}
function registerStaticEvents() {
$tbody.on('click', 'tr', function(e) {
var backingTrack = $(this).data('server-model');
// tell the server we are about to open a backing track:
rest.openBackingTrack({id: context.JK.CurrentSessionModel.id(), backing_track_path: backingTrack.name})
.done(function(response) {
var result = context.jamClient.SessionOpenBackingTrackFile(backingTrack.name, false);
console.log("BackingTrackPlay response: %o", result);
// TODO: Possibly actually check the result. Investigate
// what real client returns:
// // if(result) {
// let callers see which backing track was chosen
$dialog.data('result', backingTrack);
app.layout.closeDialog('open-backing-track-dialog');
// }
// else {
// logger.error("unable to open backing track")
// }
context.JK.CurrentSessionModel.refreshCurrentSession(true);
})
.fail(function(jqXHR) {
app.notifyServerError(jqXHR, "Unable to Open BackingTrack For Playback");
})
return false;
})
context.JK.helpBubble($whatAreBackingTracks, 'no help yet for this topic', {}, {positions:['bottom'], offsetParent: $dialog})
$whatAreBackingTracks.on('click', false) // no help yet
$displayAudioFileFolder.on('click', function(e) {
e.stopPropagation();
context.jamClient.OpenBackingTracksDirectory();
})
}
function initialize(){
var dialogBindings = {
'beforeShow' : beforeShow,
'afterHide': afterHide
};
app.bindDialog('open-backing-track-dialog', dialogBindings);
$dialog = $('#open-backing-track-dialog');
$tbody = $dialog.find('table.open-backing-tracks tbody');
$paginatorHolder = $dialog.find('.paginator-holder');
$templateOpenBackingTrackRow = $('#template-backing-track-row')
$whatAreBackingTracks = $dialog.find('.what-are-backingtracks')
$displayAudioFileFolder = $dialog.find('.display-backingtracks-folder')
registerStaticEvents();
};
this.initialize = initialize;
this.isShowing = function isShowing() { return showing; }
}
return this;
})(window,jQuery);

View File

@ -78,6 +78,23 @@
"icon_url": "/assets/content/icon_alert_big.png"
});
}
else {
// hunt for missing backing tracks; if so, mark them as silent
context._.each(openRecordingResult.backing_tracks, function(backingTrack) {
if(backingTrack.local_state == "MISSING") {
// mark this as deleted
logger.debug("marking recorded track as deleted")
rest.markRecordedBackingTrackSilent({recording_id: openRecordingResult.recording_id, backing_track_id: backingTrack.client_track_id})
.fail(function() {
app.notify({
"title": "Unable to Mark Backing Track",
"text": "A backing track was never played, but we could not tell the server to remove it from the recording.",
"icon_url": "/assets/content/icon_alert_big.png"
});
})
}
})
}
playbackControls.startMonitor();
}

View File

@ -21,6 +21,12 @@
var frameSize = 2.5;
var fakeJamClientRecordings = null;
var p2pCallbacks = null;
var metronomeActive=false;
var metronomeBPM=false;
var metronomeSound=false;
var metronomeMeter=0;
var backingTrackPath="";
var backingTrackLoop=false;
function dbg(msg) { logger.debug('FakeJamClient: ' + msg); }
@ -398,21 +404,42 @@
}
function SessionGetControlState(mixerIds, isMasterOrPersonal) {
dbg("SessionGetControlState");
var groups = [0, 1, 2, 3, 7, 9];
var groups = [0, 1, 2, 3, 3, 7, 8, 10, 11, 12];
var names = [
"FW AP Multi",
"FW AP Multi",
"FW AP Multi",
"FW AP Multi",
"",
""
"",
"",
"",
"",
""
];
var media_types = [
"Master",
"Monitor",
"AudioInputMusic",
"AudioInputChat",
"StreamOutMusic",
"UserMusicInput",
"PeerAudioInputMusic",
"PeerMediaTrack",
"JamTrack",
"MetronomeTrack"
]
var clientIds = [
"",
"",
"",
"",
"3933ebec-913b-43ab-a4d3-f21dc5f8955b",
"",
"",
"",
"",
""
];
var response = [];
@ -422,6 +449,7 @@
group_id: groups[i],
id: mixerIds[i] + (isMasterOrPersonal ? 'm' : 'p'),
master: isMasterOrPersonal,
media_type: media_types[i],
monitor: !isMasterOrPersonal,
mute: false,
name: names[i],
@ -686,6 +714,55 @@
function GetScoreWorkTimingInterval() { return {interval: 1000, backoff:60000} }
function SetScoreWorkTimingInterval(knobs) {return true;}
function SessionOpenBackingTrackFile(path, loop) {
backingTrackPath = path
backingTrackLoop = loop
}
function SessionSetBackingTrackFileLoop(path, loop) {
backingTrackPath = path
backingTrackLoop = loop
}
function SessionCloseBackingTrackFile(path) {
backingTrackPath=""
}
function SessionOpenMetronome(bpm, click, meter, mode){
console.log("Setting metronome BPM: ", bpm)
metronomeActive =true
metronomeBPM = bpm
metronomeSound = click
metronomeMeter = meter
}
//change setting - click. Mode 0: = mono, 1, = left ear, 2= right ear
function SessionSetMetronome(bpm,click,meter, mode){
SessionOpenMetronome(bpm, click, meter, mode)
}
//close everywhere
function SessionCloseMetronome(){
metronomeActive=false
}
function setMetronomeOpenCallback(callback) {
}
function getMyNetworkState() {
return {
ntp_stable: Math.random() > 0.5
}
}
function getPeerState(clientId) {
return {
ntp_stable: Math.random() > 0.5
}
}
// stun
function NetworkTestResult() { return {remote_udp_blocked: false} }
@ -717,6 +794,14 @@
fire();
}
function getBackingTrackList() {
return {backing_tracks: [
{name:"This is a really long name for a song dude.mp3", size:4283},
{name:"foo.mp3",size:325783838}
]};
}
function ClientUpdateStartUpdate(path, successCallback, failureCallback) {}
// -------------------------------
@ -977,6 +1062,20 @@
this.GetScoreWorkTimingInterval = GetScoreWorkTimingInterval;
this.SetScoreWorkTimingInterval = SetScoreWorkTimingInterval;
// Backing tracks:
this.getBackingTrackList = getBackingTrackList;
this.SessionCloseBackingTrackFile = SessionCloseBackingTrackFile;
this.SessionOpenBackingTrackFile = SessionOpenBackingTrackFile;
this.SessionSetBackingTrackFileLoop = SessionSetBackingTrackFileLoop;
// Metronome:
this.SessionCloseMetronome = SessionCloseMetronome;
this.SessionOpenMetronome = SessionOpenMetronome;
this.SessionSetMetronome = SessionSetMetronome;
this.setMetronomeOpenCallback = setMetronomeOpenCallback;
this.getMyNetworkState = getMyNetworkState;
this.getPeerState = getPeerState;
// Client Update
this.IsAppInWritableVolume = IsAppInWritableVolume;
this.ClientUpdateVersion = ClientUpdateVersion;

View File

@ -1043,6 +1043,18 @@
})
}
function markRecordedBackingTrackSilent(options) {
var recordingId = options["recording_id"];
var trackId = options["backing_track_id"];
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
data: {},
url: "/api/recordings/" + recordingId + "/backing_tracks/" + trackId + '/silent'
});
}
function getRecordedTrack(options) {
var recordingId = options["recording_id"];
var trackId = options["track_id"];
@ -1055,6 +1067,18 @@
});
}
function getRecordedBackingTrack(options) {
var recordingId = options["recording_id"];
var trackId = options["track_id"];
return $.ajax({
type: "GET",
dataType: "json",
contentType: 'application/json',
url: "/api/recordings/" + recordingId + "/backing_tracks/" + trackId
});
}
function getRecording(options) {
var recordingId = options["id"];
@ -1157,6 +1181,32 @@
})
}
function openBackingTrack(options) {
var musicSessionId = options["id"];
delete options["id"];
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/sessions/" + musicSessionId + "/backing_tracks/open",
data: JSON.stringify(options)
})
}
function closeBackingTrack(options) {
var musicSessionId = options["id"];
delete options["id"];
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/sessions/" + musicSessionId + "/backing_tracks/close",
data: JSON.stringify(options)
})
}
function openJamTrack(options) {
var musicSessionId = options["id"];
var jamTrackId = options["jam_track_id"];
@ -1185,6 +1235,32 @@
})
}
function openMetronome(options) {
var musicSessionId = options["id"];
delete options["id"];
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/sessions/" + musicSessionId + "/metronome/open",
data: JSON.stringify(options)
})
}
function closeMetronome(options) {
var musicSessionId = options["id"];
delete options["id"];
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/sessions/" + musicSessionId + "/metronome/close",
data: JSON.stringify(options)
})
}
function discardRecording(options) {
var recordingId = options["id"];
@ -1367,6 +1443,15 @@
});
}
function getBackingTracks(options) {
return $.ajax({
type: "GET",
url: '/api/backing_tracks?' + $.param(options),
dataType: "json",
contentType: 'application/json'
});
}
function addJamtrackToShoppingCart(options) {
return $.ajax({
type: "POST",
@ -1563,6 +1648,7 @@
this.stopRecording = stopRecording;
this.getRecording = getRecording;
this.getRecordedTrack = getRecordedTrack;
this.getRecordedBackingTrack = getRecordedBackingTrack;
this.getClaimedRecordings = getClaimedRecordings;
this.getClaimedRecording = getClaimedRecording;
this.updateClaimedRecording = updateClaimedRecording;
@ -1571,8 +1657,13 @@
this.claimRecording = claimRecording;
this.startPlayClaimedRecording = startPlayClaimedRecording;
this.stopPlayClaimedRecording = stopPlayClaimedRecording;
this.openJamTrack = openJamTrack;
this.openJamTrack = openJamTrack
this.openBackingTrack = openBackingTrack
this.closeBackingTrack = closeBackingTrack
this.closeMetronome = closeMetronome;
this.closeJamTrack = closeJamTrack;
this.openMetronome = openMetronome;
this.closeMetronome = closeMetronome;
this.discardRecording = discardRecording;
this.putTrackSyncChange = putTrackSyncChange;
this.createBand = createBand;
@ -1603,6 +1694,7 @@
this.updateAudioLatency = updateAudioLatency;
this.getJamtracks = getJamtracks;
this.getPurchasedJamTracks = getPurchasedJamTracks;
this.getBackingTracks = getBackingTracks;
this.addJamtrackToShoppingCart = addJamtrackToShoppingCart;
this.getShoppingCarts = getShoppingCarts;
this.removeShoppingCart = removeShoppingCart;
@ -1617,6 +1709,7 @@
this.getMount = getMount;
this.createSourceChange = createSourceChange;
this.validateUrlSite = validateUrlSite;
this.markRecordedBackingTrackSilent = markRecordedBackingTrackSilent;
return this;
};

View File

@ -4,6 +4,7 @@
context.JK = context.JK || {};
context.JK.SessionScreen = function(app) {
var TEMPOS = context.JK.TEMPOS;
var EVENTS = context.JK.EVENTS;
var MIX_MODES = context.JK.MIX_MODES;
var NAMED_MESSAGES = context.JK.NAMED_MESSAGES;
@ -38,11 +39,14 @@
var startTimeDate = null;
var startingRecording = false; // double-click guard
var claimedRecording = null;
var backing_track_path = null;
var playbackControls = null;
var promptLeave = false;
var rateSessionDialog = null;
var friendInput = null;
var sessionPageDone = null;
var metroTempo = 120;
var metroSound = "Beep";
var $recordingManagerViewer = null;
var $screen = null;
var $mixModeDropdown = null;
@ -432,6 +436,7 @@
var currentSession = sessionModel.getCurrentSession();
if(claimedRecording == null && (currentSession && currentSession.claimed_recording != null)) {
// this is a 'started with a claimed_recording' transition.
// we need to start a timer to watch for the state of the play session
@ -440,10 +445,18 @@
else if(claimedRecording && (currentSession == null || currentSession.claimed_recording == null)) {
playbackControls.stopMonitor();
}
claimedRecording = currentSession == null ? null : currentSession.claimed_recording;
if(backing_track_path == null && (currentSession && currentSession.backing_track_path != null)) {
playbackControls.startMonitor();
}
else if(backing_track_path && (currentSession == null || currentSession.backing_track_path == null)) {
playbackControls.stopMonitor();
}
backing_track_path = currentSession == null ? null : currentSession.backing_track_path;
}
function sessionChanged() {
handleTransitionsInRecordingPlayback();
@ -514,12 +527,21 @@
if ($('.session-livetracks .track').length === 0) {
$('.session-livetracks .when-empty').show();
}
if ($('.session-recordings .track').length === 0) {
$('.session-recordings .when-empty').show();
$('.session-recording-name-wrapper').hide();
$('.session-recordings .recording-controls').hide();
} else {
$('.session-recordings .when-empty').hide();
$('.session-recording-name-wrapper').show();
$('.session-recordings .recording-controls').show();
}
}
// Handle long labels:
$(".track-label").dotdotdot()
$(".session-recording-name").dotdotdot()
} // renderSession
function _initDialogs() {
configureTrackDialog.initialize();
@ -535,6 +557,7 @@
function _updateMixers() {
masterMixers = context.jamClient.SessionGetAllControlState(true);
personalMixers = context.jamClient.SessionGetAllControlState(false);
context.jamClient
//logger.debug("masterMixers", masterMixers)
//logger.debug("personalMixers", personalMixers)
@ -674,7 +697,6 @@
//logger.debug("clientId", clientId, "groupIds", groupIds, "mixers", mixers)
var foundMixers = {};
var mixers = mixMode == MIX_MODES.MASTER ? masterMixers : personalMixers;
// console.log("_groupedMixersForClientId", mixers)
$.each(mixers, function(index, mixer) {
if (mixer.client_id === clientId) {
for (var i=0; i<groupIds.length; i++) {
@ -811,11 +833,14 @@
}
function _renderLocalMediaTracks() {
//console.log("_renderLocalMediaTracks")
// local media mixers come in different groups (MediaTrack, JamTrack, Metronome), but peer mixers are always PeerMediaTrackGroup
var localMediaMixers = _mixersForGroupIds([ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup], MIX_MODES.MASTER);
var peerLocalMediaMixers = _mixersForGroupId(ChannelGroupIds.PeerMediaTrackGroup, MIX_MODES.MASTER);
var recordedBackingTracks = sessionModel.recordedBackingTracks();
var backingTracks = sessionModel.backingTracks();
// with mixer info, we use these to decide what kind of tracks are open in the backend
// each mixer has a media_type field, which describes the type of media track it is.
@ -836,31 +861,61 @@
var metronomeTrackMixers = [];
var adhocTrackMixers = [];
function groupByType(mixers) {
function groupByType(mixers, isLocalMixer) {
context._.each(mixers, function(mixer) {
var mediaType = mixer.media_type;
var groupId = mixer.group_id;
// mediaType == null is for backwards compat with older clients. Can be removed soon
if(mediaType == null || mediaType == "" || mediaType == 'RecordingTrack') {
recordingTrackMixers.push(mixer)
}
else if(mediaType == 'BackingTrack') {
// additional check; if we can match an id in backing tracks or recorded backing track,
// we need to remove it from the recorded track set, but move it to the backing track set
var isBackingTrack = false
if(recordedBackingTracks) {
context._.each(recordedBackingTracks, function (recordedBackingTrack) {
if (mixer.id == 'L' + recordedBackingTrack.client_track_id) {
isBackingTrack = true;
return false; // break
}
})
}
if(backingTracks) {
context._.each(backingTracks, function (backingTrack) {
if (mixer.id == 'L' + backingTrack.client_track_id) {
isBackingTrack = true;
return false; // break
}
})
}
if(isBackingTrack) {
backingTrackMixers.push(mixer)
}
else {
recordingTrackMixers.push(mixer);
}
} else if(mediaType == 'PeerMediaTrack' || mediaType == 'BackingTrack') {
// BackingTrack
backingTrackMixers.push(mixer);
}
else if(mediaType == 'MetronomeTrack') {
} else if(mediaType == 'MetronomeTrack' || groupId==ChannelGroupIds.MetronomeGroup) {
// Metronomes come across with a blank media type, so check group_id:
metronomeTrackMixers.push(mixer);
}
else if(mediaType == 'JamTrack') {
} else if(mediaType == 'JamTrack') {
jamTrackMixers.push(mixer);
}
else {
mixer.group_id == ChannelGroupIds.MediaTrackGroup;
} else if(mediaType == null || mediaType == "" || mediaType == 'RecordingTrack') {
// mediaType == null is for backwards compat with older clients. Can be removed soon
recordingTrackMixers.push(mixer)
} else {
logger.warn("Unknown track type: " + mediaType)
adhocTrackMixers.push(mixer);
}
});
}
groupByType(localMediaMixers);
groupByType(peerLocalMediaMixers);
groupByType(localMediaMixers, true);
groupByType(peerLocalMediaMixers, false);
if(recordingTrackMixers.length > 0) {
renderRecordingTracks(recordingTrackMixers)
@ -872,20 +927,131 @@
renderJamTracks(jamTrackMixers);
}
if(metronomeTrackMixers.length > 0) {
renderMetronomeTracks(jamTrackMixers);
renderMetronomeTracks(metronomeTrackMixers);
}
if(adhocTrackMixers.length > 0) {
logger.warn("some tracks are open that we don't know how to show")
}
}
// this method is pretty complicated because it forks on a key bit of state:
// sessionModel.isPlayingRecording()
// a backing track opened as part of a recording has a different behavior and presence on the server (recording.recorded_backing_tracks)
// than a backing track opend ad-hoc (connection.backing_tracks)
function renderBackingTracks(backingTrackMixers) {
logger.error("do not know how to draw backing tracks yet")
var backingTracks = []
if(sessionModel.isPlayingRecording()) {
// only return managed mixers for recorded backing tracks
backingTrackMixers = context._.filter(backingTrackMixers, function(mixer){return mixer.managed || mixer.managed === undefined})
backingTracks = sessionModel.recordedBackingTracks();
}
else {
// only return un-managed (ad-hoc) mixers for normal backing tracks
backingTracks = sessionModel.backingTracks();
backingTrackMixers = context._.filter(backingTrackMixers, function(mixer){return !mixer.managed})
if(backingTrackMixers.length > 1) {
app.notify({
title: "Multiple Backing Tracks Encountered",
text: "Only one backing track can be open a time.",
icon_url: "/assets/content/icon_alert_big.png"
});
return false;
}
}
var noCorrespondingTracks = false;
$.each(backingTrackMixers, function(index, mixer) {
console.log("hunting for backing tracks:", backingTrackMixers, backingTracks)
// find the track or tracks that correspond to the mixer
var correspondingTracks = []
var noCorrespondingTracks = false;
if(sessionModel.isPlayingRecording()) {
$.each(backingTracks, function (i, backingTrack) {
if(mixer.persisted_track_id == backingTrack.client_track_id || // occurs if this client is the one that opened the track
mixer.id == 'L' + backingTrack.client_track_id) { // occurs if this client is a remote participant
correspondingTracks.push(backingTrack)
}
});
}
else
{
// if this is just an open backing track, then we can assume that the 1st backingTrackMixer is ours
correspondingTracks.push(backingTracks[0])
}
if (correspondingTracks.length == 0) {
noCorrespondingTracks = true;
logger.debug("renderBackingTracks: could not map backing tracks")
app.notify({
title: "Unable to Open Backing Track",
text: "Could not correlate server and client tracks",
icon_url: "/assets/content/icon_alert_big.png"
});
return false;
}
// now we have backing track and mixer in hand; we can render
var backingTrack = correspondingTracks[0]
// pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between JamTrack vs Peer)
// if it's a locally opened track (MediaTrackGroup), then we can say this person is the opener
var isOpener = mixer.group_id == ChannelGroupIds.MediaTrackGroup;
var shortFilename = context.JK.getNameOfFile(backingTrack.filename);
if(!sessionModel.isPlayingRecording()) {
// if a recording is being played back, do not set this header, because renderRecordedTracks already did
// ugly.
$('.session-recording-name').text(shortFilename);
}
var instrumentIcon = context.JK.getInstrumentIcon45(backingTrack.instrument_id);
var photoUrl = "/assets/content/icon_recording.png";
// Default trackData to participant + no Mixer state.
var trackData = {
trackId: backingTrack.id,
clientId: backingTrack.client_id,
name: 'Backing',
filename: backingTrack.filename,
instrumentIcon: instrumentIcon,
avatar: photoUrl,
latency: "good",
gainPercent: 0,
muteClass: 'muted',
showLoop: isOpener && !sessionModel.isPlayingRecording(),
mixerId: "",
avatarClass: 'avatar-recording',
preMasteredClass: ""
};
var gainPercent = percentFromMixerValue(
mixer.range_low, mixer.range_high, mixer.volume_left);
var muteClass = "enabled";
if (mixer.mute) {
muteClass = "muted";
}
trackData.gainPercent = gainPercent;
trackData.muteClass = muteClass;
trackData.mixerId = mixer.id; // the master mixer controls the volume control for recordings (no personal controls in either master or personal mode)
trackData.vuMixerId = mixer.id; // the master mixer controls the VUs for recordings (no personal controls in either master or personal mode)
trackData.muteMixerId = mixer.id; // the master mixer controls the mute for recordings (no personal controls in either master or personal mode)
if (sessionModel.isPersonalMixMode() || !isOpener) {
trackData.mediaControlsDisabled = true;
trackData.mediaTrackOpener = isOpener;
}
_addRecordingTrack(trackData, mixer);
});
}
function renderJamTracks(jamTrackMixers) {
log.debug("rendering jam tracks")
console.log("rendering jam tracks")
var jamTracks = sessionModel.jamTracks();
// pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between JamTrack vs Peer)
@ -901,7 +1067,6 @@
var preMasteredClass = "";
// find the track or tracks that correspond to the mixer
var correspondingTracks = []
console.log("mixer", mixer)
$.each(jamTracks, function(i, jamTrack) {
if(mixer.id.indexOf("L") == 0) {
if(mixer.id.substring(1) == jamTrack.id) {
@ -909,7 +1074,7 @@
}
else {
// this should not be possible
alert("Invalid state: the recorded track had neither persisted_track_id or persisted_client_id");
alert("Invalid state: the backing track had neither persisted_track_id or persisted_client_id");
}
}
});
@ -981,13 +1146,107 @@
}
function renderMetronomeTracks(metronomeTrackMixers) {
logger.error("do not know how to draw metronome tracks yet")
var metronomeActive = sessionModel.metronomeActive();
console.log("rendering metronome track",metronomeActive)
// pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between JamTrack vs Peer)
// if it's a locally opened track (MediaTrackGroup), then we can say this person is the opener
var isOpener = metronomeTrackMixers[0].group_id == ChannelGroupIds.MediaTrackGroup;
var name = "Metronome"
// using the server's info in conjuction with the client's, draw the recording tracks
if(metronomeActive && metronomeTrackMixers.length > 0) {
var metronome = {active: metronomeActive}
$('.session-recording-name').text(name);//sessionModel.getCurrentSession().backing_track_path);
var noCorrespondingTracks = false;
var mixer = metronomeTrackMixers[0]
var preMasteredClass = "";
// find the track or tracks that correspond to the mixer
var correspondingTracks = []
correspondingTracks.push(metronome);
if(correspondingTracks.length == 0) {
noCorrespondingTracks = true;
app.notify({
title: "Unable to Open Metronome",
text: "Could not correlate server and client tracks",
icon_url: "/assets/content/icon_metronome_small.png"});
return false;
}
// prune found recorded tracks
// Metronomes = $.grep(Metronomes, function(value) {
// return $.inArray(value, correspondingTracks) < 0;
// });
var oneOfTheTracks = correspondingTracks[0];
var instrumentIcon = context.JK.getInstrumentIcon45(oneOfTheTracks.instrument_id);
var photoUrl = "/assets/content/icon_metronome_small.png";
// var trackData = {
// trackId: oneOfTheTracks.id,
// clientId: oneOfTheTracks.client_id,
// name: "Tempo",
// instrumentIcon: photoUrl,
// avatar: instrumentIcon,
// latency: "good",
// gainPercent: 0,
// muteClass: 'hidden',
// mixerId: "",
// avatarClass : 'avatar-recording',
// preMasteredClass: "",
// hideVU: true,
// faderChanged : tempoFaderChanged,
// showMetronomeControls: true
// };
// _addRecordingTrack(trackData);
// Default trackData to participant + no Mixer state.
var trackData = {
trackId: "MS" + oneOfTheTracks.id,
clientId: oneOfTheTracks.client_id,
name: "Metronome",
instrumentIcon: photoUrl,
avatar: instrumentIcon,
latency: "good",
gainPercent: 0,
muteClass: 'muted',
mixerId: "",
avatarClass : 'avatar-recording',
preMasteredClass: "",
showMetronomeControls: true
};
var gainPercent = percentFromMixerValue(
mixer.range_low, mixer.range_high, mixer.volume_left);
var muteClass = "enabled";
if (mixer.mute) {
muteClass = "muted";
}
trackData.gainPercent = gainPercent;
trackData.muteClass = muteClass;
trackData.mixerId = mixer.id; // the master mixer controls the volume control for recordings (no personal controls in either master or personal mode)
trackData.vuMixerId = mixer.id; // the master mixer controls the VUs for recordings (no personal controls in either master or personal mode)
trackData.muteMixerId = mixer.id; // the master mixer controls the mute for recordings (no personal controls in either master or personal mode)
if(sessionModel.isPersonalMixMode() || !isOpener) {
//trackData.mediaControlsDisabled = true;
trackData.mediaTrackOpener = isOpener;
}
_addRecordingTrack(trackData, mixer);
}// if
setFormFromMetronome()
}
function renderRecordingTracks(recordingMixers) {
// get the server's info for the recording
var recordedTracks = sessionModel.recordedTracks();
var recordedBackingTracks = sessionModel.recordedBackingTracks();
// pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between Local vs Peer)
// if it's a locally opened track (MediaTrackGroup), then we can say this person is the opener
@ -998,6 +1257,7 @@
if(recordedTracks) {
$('.session-recording-name').text(sessionModel.getCurrentSession().claimed_recording.name);
console.log("renderRecordingTracks", recordingMixers, recordedTracks)
var noCorrespondingTracks = false;
$.each(recordingMixers, function(index, mixer) {
var preMasteredClass = "";
@ -1023,6 +1283,7 @@
if(correspondingTracks.length == 0) {
noCorrespondingTracks = true;
logger.debug("unable to correlate all recorded tracks", recordingMixers, recordedTracks)
app.notify({
title: "Unable to Open Recording",
text: "Could not correlate server and client tracks",
@ -1075,7 +1336,7 @@
trackData.mediaControlsDisabled = true;
trackData.mediaTrackOpener = isOpener;
}
_addRecordingTrack(trackData);
_addRecordingTrack(trackData, mixer);
});
if(!noCorrespondingTracks && recordedTracks.length > 0) {
@ -1132,6 +1393,7 @@
var mixMode = sessionModel.getMixMode();
if(myTrack) {
// when it's your track, look it up by the backend resource ID
mixer = getMixerByTrackId(track.client_track_id, mixMode)
vuMixer = mixer;
@ -1312,16 +1574,35 @@
}
var $track = $(trackSelector);
// Set mixer-id attributes and render VU/Fader
context.JK.VuHelpers.renderVU(vuLeftSelector, vuOpts);
$track.find('.track-vu-left').attr('mixer-id', track.vuMixerId + '_vul').data('groupId', groupId)
context.JK.VuHelpers.renderVU(vuRightSelector, vuOpts);
$track.find('.track-vu-right').attr('mixer-id', track.vuMixerId + '_vur').data('groupId', groupId)
if (!track.hideVU) {
context.JK.VuHelpers.renderVU(vuLeftSelector, vuOpts);
$track.find('.track-vu-left').attr('mixer-id', track.vuMixerId + '_vul').data('groupId', groupId)
context.JK.VuHelpers.renderVU(vuRightSelector, vuOpts);
$track.find('.track-vu-right').attr('mixer-id', track.vuMixerId + '_vur').data('groupId', groupId)
}
if (track.showMetronomeControls) {
$track.find('.metronome-selects').removeClass("hidden")
} else {
$track.find('.metronome-selects').addClass("hidden")
}
// if (track.showMetroSound) {
// $track.find('.metro-sound-select').removeClass("hidden")
// }
context.JK.FaderHelpers.renderFader($fader, faderOpts);
// Set gain position
context.JK.FaderHelpers.setFaderValue(mixerId, gainPercent);
$fader.on('fader_change', faderChanged);
return $track;
if(track.faderChanged) {
$fader.on('fader_change', track.faderChanged);
} else {
$fader.on('fader_change', faderChanged);
}
return $track;
}
// Function called on an interval when participants change. Mixers seem to
@ -1470,13 +1751,10 @@
function _addRecordingTrack(trackData) {
function _addRecordingTrack(trackData, mixer) {
var parentSelector = '#session-recordedtracks-container';
var $destination = $(parentSelector);
$('.session-recordings .when-empty').hide();
$('.session-recording-name-wrapper').show();
$('.session-recordings .recording-controls').show();
var template = $('#template-session-track').html();
var newTrack = $(context.JK.fillTemplate(template, trackData));
$destination.append(newTrack);
@ -1492,6 +1770,22 @@
if(trackData.mediaControlsDisabled) {
$trackIconMute.data('media-controls-disabled', true).data('media-track-opener', trackData.mediaTrackOpener)
}
$trackIconMute.data('mixer', mixer).data('opposite-mixer', null)
if(trackData.showLoop) {
var $trackIconLoop = $track.find('.track-icon-loop')
var $trackIconLoopCheckbox = $trackIconLoop.find('input')
context.JK.checkbox($trackIconLoopCheckbox)
$trackIconLoopCheckbox.on('ifChanged', function() {
var loop = $trackIconLoopCheckbox.is(':checked')
logger.debug("Looping: ", loop, trackData.filename);
context.jamClient.SessionSetBackingTrackFileLoop(trackData.filename, loop)
});
$trackIconLoop.show()
}
// is this used?
tracks[trackData.trackId] = new context.JK.SessionTrack(trackData.clientId);
}
@ -1517,6 +1811,28 @@
});
}
// function tempoFaderChanged(e, data) {
// var $target = $(this);
// var faderId = $target.attr('mixer-id');
// var groupId = $target.data('groupId');
// var mixerIds = faderId.split(',');
// $.each(mixerIds, function(i,v) {
// // TODO Interpolate tempo values if we decide to go this way:
// if(groupId == ChannelGroupIds.UserMusicInputGroup) {
// // there may be other mixers with this same ID in the case of a Peer Music Stream, so update them as well
// }
// });
// }
function handleMetronomeCallback(args) {
console.log("MetronomeCallback: ", args)
metroTempo = args.bpm
// This isn't actually there, so we rely on the metroSound as set from select on form:
// metroSound = args.sound
context.JK.CurrentSessionModel.refreshCurrentSession(true);
}
function handleVolumeChangeCallback(mixerId, isLeft, value, isMuted) {
// Visually update mixer
// There is no need to actually set the back-end mixer value as the
@ -1592,7 +1908,27 @@
}
}
function handleBackingTrackSelectedCallback(result) {
if(result.success) {
logger.debug("backing track selected: " + result.file);
rest.openBackingTrack({id: context.JK.CurrentSessionModel.id(), backing_track_path: result.file})
.done(function(response) {
var openResult = context.jamClient.SessionOpenBackingTrackFile(result.file, false);
console.log("BackingTrackPlay response: %o", openResult);
//context.JK.CurrentSessionModel.refreshCurrentSession(true);
sessionModel.setBackingTrack(result.file);
})
.fail(function(jqXHR) {
app.notifyServerError(jqXHR, "Unable to Open BackingTrack For Playback");
})
}
else {
logger.debug("no backing track selected")
}
}
function deleteSession(evt) {
var sessionId = $(evt.currentTarget).attr("action-id");
if (sessionId) {
@ -1911,6 +2247,27 @@
.fail(app.ajaxError);
}
function openBackingTrack(e) {
// just ignore the click if they are currently recording for now
if(sessionModel.recordingModel.isRecording()) {
app.notify({
"title": "Currently Recording",
"text": "You can't open a backing track while creating a recording.",
"icon_url": "/assets/content/icon_alert_big.png"
});
return false;
}
context.jamClient.ShowSelectBackingTrackDialog("window.JK.HandleBackingTrackSelectedCallback");
//app.layout.showDialog('open-backing-track-dialog').one(EVENTS.DIALOG_CLOSED, function(e, data) {
// if(!data.cancel && data.result){
// sessionModel.setBackingTrack(data.result);
// }
//})
return false;
}
function openJamTrack(e) {
// just ignore the click if they are currently recording for now
if(sessionModel.recordingModel.isRecording()) {
@ -1927,6 +2284,95 @@
return false;
}
function openBackingTrackFile(e) {
// just ignore the click if they are currently recording for now
if(sessionModel.recordingModel.isRecording()) {
app.notify({
"title": "Currently Recording",
"text": "You can't open a backing track while creating a recording.",
"icon_url": "/assets/content/icon_alert_big.png"
});
return false;
} else {
context.jamClient.openBackingTrackFile(sessionModel.backing_track)
//context.JK.CurrentSessionModel.refreshCurrentSession(true);
}
return false;
}
function unstableNTPClocks() {
var unstable = []
// This should be handled in the below loop, actually:
// var map = context.jamClient.getMyNetworkState()
// if (!map.ntp_stable) {
// unstable.push("self");
// }
var map;
$.each(sessionModel.participants(), function(index, participant) {
map = context.jamClient.getPeerState(participant.client_id)
if (!map.ntp_stable) {
var name = participant.user.name;
if (!(name)) {
name = participant.user.first_name + ' ' + participant.user.last_name;
}
if (app.clientId == participant.client_id) {
name += " (This computer)"
}
unstable.push(name)
}
});
return unstable
}
function openMetronome(e) {
// just ignore the click if they are currently recording for now
if(sessionModel.recordingModel.isRecording()) {
app.notify({
"title": "Currently Recording",
"text": "You can't open a metronome while creating a recording.",
"icon_url": "/assets/content/icon_alert_big.png"
});
return false;
} else {
var unstable = unstableNTPClocks()
if (unstable.length > 0) {
var names = unstable.join(", ")
console.log("Unstable clocks: ", names, unstable)
app.notify({
"title": "Couldn't open metronome",
"text": "The metronome feature requires that every user's computer in the session must agree on the current time. The computers of " + names + " have not successfully synchronized to the current time. The JamKazam service is trying to automatically correct this error condition. Please close this message, wait about 10 seconds, and then try opening the metronome again. If this problem persists after a couple of attempts, we recommend that the unsynchronized users restart the JamKazam application. If this error persists after a restart, please have the users with the issue contact support@jamkazam.com.",
"icon_url": "/assets/content/icon_alert_big.png"
});
} else {
rest.openMetronome({id: sessionModel.id()})
.done(function() {
context.jamClient.SessionOpenMetronome(120, "Click", 1, 0)
context.JK.CurrentSessionModel.refreshCurrentSession(true)
context.JK.CurrentSessionModel.refreshCurrentSession(true)
})
.fail(function(jqXHR) {
console.log(jqXHR, jqXHR)
app.notify({
"title": "Couldn't open metronome",
"text": "Couldn't inform the server to open metronome. msg=" + jqXHR.responseText,
"icon_url": "/assets/content/icon_alert_big.png"
});
});
}
return false;
}
}
function openRecording(e) {
// just ignore the click if they are currently recording for now
if(sessionModel.recordingModel.isRecording()) {
@ -1952,15 +2398,41 @@
else if(sessionModel.jamTracks()) {
closeJamTrack();
}
else {
logger.error("don't know how to close open media (backing track?)");
else if(sessionModel.backingTrack() && sessionModel.backingTrack().path) {
closeBackingTrack();
}
else if(sessionModel.metronomeActive()) {
closeMetronomeTrack();
}
else {
logger.error("don't know how to close open media");
}
return false;
}
function closeBackingTrack() {
rest.closeBackingTrack({id: sessionModel.id()})
.done(function() {
//sessionModel.refreshCurrentSession(true);
})
.fail(function(jqXHR) {
app.notify({
"title": "Couldn't Close BackingTrack",
"text": "Couldn't inform the server to close BackingTrack. msg=" + jqXHR.responseText,
"icon_url": "/assets/content/icon_alert_big.png"
});
});
// '' closes all open backing tracks
context.jamClient.SessionCloseBackingTrackFile('');
return false;
}
function closeJamTrack() {
rest.closeJamTrack({id: sessionModel.id()})
.done(function() {
sessionModel.refreshCurrentSession();
sessionModel.refreshCurrentSession(true);
})
.fail(function(jqXHR) {
app.notify({
@ -1975,10 +2447,28 @@
return false;
}
function closeMetronomeTrack() {
rest.closeMetronome({id: sessionModel.id()})
.done(function() {
context.jamClient.SessionCloseMetronome();
sessionModel.refreshCurrentSession(true);
})
.fail(function(jqXHR) {
app.notify({
"title": "Couldn't Close MetronomeTrack",
"text": "Couldn't inform the server to close MetronomeTrack. msg=" + jqXHR.responseText,
"icon_url": "/assets/content/icon_alert_big.png"
});
});
return false;
}
function closeRecording() {
rest.stopPlayClaimedRecording({id: sessionModel.id(), claimed_recording_id: sessionModel.getCurrentSession().claimed_recording.id})
.done(function() {
sessionModel.refreshCurrentSession();
.done(function(response) {
//sessionModel.refreshCurrentSession(true);
// update session info
context.JK.CurrentSessionModel.updateSession(response);
})
.fail(function(jqXHR) {
app.notify({
@ -2022,12 +2512,41 @@
sessionId);
inviteMusiciansUtil.loadFriends();
$(friendInput).show();
}
}
function onMixerModeChanged(e, data)
{
function setFormFromMetronome() {
$("select.metro-tempo").val(metroTempo)
$("select.metro-sound").val(metroSound)
}
function setMetronomeFromForm() {
var tempo = $("select.metro-tempo:visible option:selected").val()
var sound = $("select.metro-sound:visible option:selected").val()
var t = parseInt(tempo)
var s
if (tempo==NaN || tempo==0 || tempo==null) {
t = 120
}
if (sound==null || typeof(sound)=='undefined' || sound=="") {
s = "click"
} else {
s = sound
}
console.log("Setting tempo and sound:", t, s)
metroTempo = t
metroSound = s
context.jamClient.SessionSetMetronome(t, s, 1, 0)
}
function onMetronomeChanged(e, data) {
setMetronomeFromForm()
}
function onMixerModeChanged(e, data) {
$mixModeDropdown.easyDropDown('select', data.mode, true);
setTimeout(renderSession, 1);
}
@ -2046,7 +2565,7 @@
return true;
}
function events() {
function events() {
$('#session-leave').on('click', sessionLeave);
$('#session-resync').on('click', sessionResync);
$('#session-contents').on("click", '[action="delete"]', deleteSession);
@ -2054,6 +2573,8 @@
$('#recording-start-stop').on('click', startStopRecording);
$('#open-a-recording').on('click', openRecording);
$('#open-a-jamtrack').on('click', openJamTrack);
$('#open-a-backingtrack').on('click', openBackingTrack);
$('#open-a-metronome').on('click', openMetronome);
$('#session-invite-musicians').on('click', inviteMusicians);
$('#session-invite-musicians2').on('click', inviteMusicians);
$('#track-settings').click(function() {
@ -2070,6 +2591,7 @@
$(friendInput).focus(function() { $(this).val(''); })
$(document).on(EVENTS.MIXER_MODE_CHANGED, onMixerModeChanged)
$mixModeDropdown.change(onUserChangeMixMode)
$(document).on("change", ".metronome-select", onMetronomeChanged)
}
this.initialize = function(localRecordingsDialogInstance, recordingFinishedDialogInstance, friendSelectorDialog) {
@ -2080,6 +2602,7 @@
context.jamClient.SetVURefreshRate(150);
context.jamClient.RegisterVolChangeCallBack("JK.HandleVolumeChangeCallback");
playbackControls = new context.JK.PlaybackControls($('.session-recordings .recording-controls'));
context.jamClient.setMetronomeOpenCallback("JK.HandleMetronomeCallback")
var screenBindings = {
'beforeShow': beforeShow,
@ -2121,7 +2644,9 @@
}
context.JK.HandleVolumeChangeCallback = handleVolumeChangeCallback;
context.JK.HandleMetronomeCallback = handleMetronomeCallback;
context.JK.HandleBridgeCallback = handleBridgeCallback;
context.JK.HandleBackingTrackSelectedCallback = handleBackingTrackSelectedCallback;
};
})(window,jQuery);

View File

@ -34,6 +34,8 @@
var sessionPageEnterTimeout = null;
var startTime = null;
var joinDeferred = null;
var previousBackingTracks = [];
var openBackingTrack = null;
var mixerMode = MIX_MODES.PERSONAL;
@ -67,7 +69,7 @@
function isPlayingRecording() {
// this is the server's state; there is no guarantee that the local tracks
// requested from the backend will have corresponding track information
return currentSession && currentSession.claimed_recording;
return !!(currentSession && currentSession.claimed_recording);
}
function recordedTracks() {
@ -79,6 +81,28 @@
}
}
function recordedBackingTracks() {
if(currentSession && currentSession.claimed_recording) {
return currentSession.claimed_recording.recording.recorded_backing_tracks
}
else {
return null;
}
}
function backingTracks() {
var backingTracks = []
// this may be wrong if we loosen the idea that only one person can have a backing track open.
// but for now, the 1st person we find with a backing track open is all there is to find...
context._.each(participants(), function(participant) {
if(participant.backing_tracks.length > 0) {
backingTracks = participant.backing_tracks;
return false; // break
}
})
return backingTracks;
}
function jamTracks() {
if(currentSession && currentSession.jam_track) {
return currentSession.jam_track.tracks
@ -88,6 +112,27 @@
}
}
function backingTrack() {
if(currentSession) {
// TODO: objectize this for VRFS-2665, VRFS-2666, VRFS-2667, VRFS-2668
return {
path: currentSession.backing_track_path
}
}
else {
return null;
}
}
function metronomeActive() {
if(currentSession) {
return currentSession.metronome_active
}
else {
return null;
}
}
function creatorId() {
if(!currentSession) {
throw "creator is not known"
@ -303,6 +348,8 @@
}
currentSessionId = null;
currentParticipants = {}
previousBackingTracks = []
openBackingTrack = null
}
// you should only update currentSession with this function
@ -321,6 +368,25 @@
}
}
function updateSession(response) {
updateSessionInfo(response, null, true);
}
function updateSessionInfo(response, callback, force) {
if(force === true || currentTrackChanges < response.track_changes_counter) {
logger.debug("updating current track changes from %o to %o", currentTrackChanges, response.track_changes_counter)
currentTrackChanges = response.track_changes_counter;
sendClientParticipantChanges(currentSession, response);
updateCurrentSession(response);
if(callback != null) {
callback();
}
}
else {
logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter);
}
}
/**
* Reload the session data from the REST server, calling
* the provided callback when complete.
@ -344,18 +410,7 @@
type: "GET",
url: url,
success: function(response) {
if(force === true || currentTrackChanges < response.track_changes_counter) {
logger.debug("updating current track changes from %o to %o", currentTrackChanges, response.track_changes_counter)
currentTrackChanges = response.track_changes_counter;
sendClientParticipantChanges(currentSession, response);
updateCurrentSession(response);
if(callback != null) {
callback();
}
}
else {
logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter);
}
updateSessionInfo(response, callback, force);
},
error: function(jqXHR) {
if(jqXHR.status != 404) {
@ -536,6 +591,46 @@
return mixerMode;
}
function syncTracks(backingTracks) {
// double check that we are in session, since a bunch could have happened since then
if(!inSession()) {
logger.debug("dropping queued up sync tracks because no longer in session");
return null;
}
// this is a local change to our tracks. we need to tell the server about our updated track information
var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient);
// backingTracks can be passed in as an optimization, so that we don't hit the backend excessively
if(backingTracks === undefined ) {
backingTracks = context.JK.TrackHelpers.getBackingTracks(context.jamClient);
}
// create a trackSync request based on backend data
var syncTrackRequest = {};
syncTrackRequest.client_id = app.clientId;
syncTrackRequest.tracks = inputTracks;
syncTrackRequest.backing_tracks = backingTracks;
syncTrackRequest.id = id();
return rest.putTrackSyncChange(syncTrackRequest)
.done(function() {
})
.fail(function(jqXHR) {
if(jqXHR.status != 404) {
app.notify({
"title": "Can't Sync Local Tracks",
"text": "The client is unable to sync local track information with the server. You should rejoin the session to ensure a good experience.",
"icon_url": "/assets/content/icon_alert_big.png"
});
}
else {
logger.debug("Unable to sync local tracks because session is gone.")
}
})
}
function onWebsocketDisconnected(in_error) {
// kill the streaming of the session immediately
if(currentSessionId) {
@ -678,45 +773,25 @@
// wait until we are fully in session before trying to sync tracks to server
if(joinDeferred) {
joinDeferred.done(function() {
// double check that we are in session, since a bunch could have happened since then
if(!inSession()) {
logger.debug("dropping queued up sync tracks because no longer in session");
return;
}
// this is a local change to our tracks. we need to tell the server about our updated track information
var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient);
// create a trackSync request based on backend data
var syncTrackRequest = {};
syncTrackRequest.client_id = app.clientId;
syncTrackRequest.tracks = inputTracks;
syncTrackRequest.id = id();
rest.putTrackSyncChange(syncTrackRequest)
.done(function() {
})
.fail(function(jqXHR) {
if(jqXHR.status != 404) {
app.notify({
"title": "Can't Sync Local Tracks",
"text": "The client is unable to sync local track information with the server. You should rejoin the session to ensure a good experience.",
"icon_url": "/assets/content/icon_alert_big.png"
});
}
else {
logger.debug("Unable to sync local tracks because session is gone.")
}
})
syncTracks();
})
}
}, 100);
}
else if(inSession() && (text == 'RebuildMediaControl' || text == 'RebuildRemoteUserControl')) {
refreshCurrentSession(true);
var backingTracks = context.JK.TrackHelpers.getBackingTracks(context.jamClient);
// the way we know if backing tracks changes, or recordings are opened, is via this event.
// but we want to report to the user when backing tracks change; so we need to detect change on our own
if(previousBackingTracks != backingTracks) {
logger.debug("backing tracks changed")
syncTracks(backingTracks);
}
else {
refreshCurrentSession(true);
}
}
else if(inSession() && (text == 'Global Peer Input Mixer Mode')) {
setMixerMode(MIX_MODES.MASTER);
@ -729,6 +804,10 @@
// Public interface
this.id = id;
this.start = start;
this.backingTrack = backingTrack;
this.backingTracks = backingTracks;
this.recordedBackingTracks = recordedBackingTracks;
this.metronomeActive = metronomeActive;
this.setUserTracks = setUserTracks;
this.recordedTracks = recordedTracks;
this.jamTracks = jamTracks;
@ -736,6 +815,7 @@
this.joinSession = joinSession;
this.leaveCurrentSession = leaveCurrentSession;
this.refreshCurrentSession = refreshCurrentSession;
this.updateSession = updateSession;
this.subscribe = subscribe;
this.participantForClientId = participantForClientId;
this.isPlayingRecording = isPlayingRecording;
@ -767,6 +847,12 @@
this.getParticipant = function(clientId) {
return participantsEverSeen[clientId]
};
this.setBackingTrack = function(backingTrack) {
openBackingTrack = backingTrack;
};
this.getBackingTrack = function() {
return openBackingTrack;
};
// call to report if the current user was able to establish audio with the specified clientID
this.setAudioEstablished = function(clientId, audioEstablished) {

View File

@ -27,16 +27,19 @@ context.JK.SyncViewer = class SyncViewer
@list = @root.find('.list')
@logList = @root.find('.log-list')
@templateRecordedTrack = $('#template-sync-viewer-recorded-track')
@templateRecordedBackingTrack = $('#template-sync-viewer-recorded-backing-track')
@templateStreamMix = $('#template-sync-viewer-stream-mix')
@templateMix = $('#template-sync-viewer-mix')
@templateNoSyncs = $('#template-sync-viewer-no-syncs')
@templateRecordingWrapperDetails = $('#template-sync-viewer-recording-wrapper-details')
@templateHoverRecordedTrack = $('#template-sync-viewer-hover-recorded-track')
@templateHoverRecordedBackingTrack = $('#template-sync-viewer-hover-recorded-backing-track')
@templateHoverMix = $('#template-sync-viewer-hover-mix')
@templateDownloadReset = $('#template-sync-viewer-download-progress-reset')
@templateUploadReset = $('#template-sync-viewer-upload-progress-reset')
@templateGenericCommand = $('#template-sync-viewer-generic-command')
@templateRecordedTrackCommand = $('#template-sync-viewer-recorded-track-command')
@templateRecordedBackingTrackCommand = $('#template-sync-viewer-recorded-backing-track-command')
@templateLogItem = $('#template-sync-viewer-log-item')
@tabSelectors = @root.find('.dialog-tabs .tab')
@tabs = @root.find('.tab-content')
@ -50,7 +53,8 @@ context.JK.SyncViewer = class SyncViewer
them_upload_soon: 'them-upload-soon'
missing: 'missing',
me_uploaded: 'me-uploaded',
them_uploaded: 'them-uploaded'
them_uploaded: 'them-uploaded',
not_mine: 'not-mine'
}
@clientStates = {
unknown: 'unknown',
@ -58,7 +62,8 @@ context.JK.SyncViewer = class SyncViewer
hq: 'hq',
sq: 'sq',
missing: 'missing',
discarded: 'discarded'
discarded: 'discarded',
not_mine: 'not-mine'
}
throw "no sync-viewer" if not @root.exists()
@ -329,12 +334,138 @@ context.JK.SyncViewer = class SyncViewer
$clientRetry.hide()
$uploadRetry.hide()
updateBackingTrackState: ($track) =>
clientInfo = $track.data('client-info')
serverInfo = $track.data('server-info')
myTrack = serverInfo.user.id == context.JK.currentUserId
# determine client state
clientStateMsg = 'UNKNOWN'
clientStateClass = 'unknown'
clientState = @clientStates.unknown
if serverInfo.mine
if serverInfo.download.should_download
if serverInfo.download.too_many_downloads
clientStateMsg = 'EXCESS DOWNLOADS'
clientStateClass = 'error'
clientState = @clientStates.too_many_uploads
else
if clientInfo?
if clientInfo.local_state == 'HQ'
clientStateMsg = 'HIGHEST QUALITY'
clientStateClass = 'hq'
clientState = @clientStates.hq
else if clientInfo.local_state == 'MISSING'
clientStateMsg = 'MISSING'
clientStateClass = 'missing'
clientState = @clientStates.missing
else
clientStateMsg = 'MISSING'
clientStateClass = 'missing'
clientState = @clientStates.missing
else
clientStateMsg = 'DISCARDED'
clientStateClass = 'discarded'
clientState = @clientStates.discarded
else
clientStateMsg = 'NOT MINE'
clientStateClass = 'not_mine'
clientState = @clientStates.not_mine
# determine upload state
uploadStateMsg = 'UNKNOWN'
uploadStateClass = 'unknown'
uploadState = @uploadStates.unknown
if serverInfo.mine
if !serverInfo.fully_uploaded
if serverInfo.upload.too_many_upload_failures
uploadStateMsg = 'UPLOAD FAILURE'
uploadStateClass = 'error'
uploadState = @uploadStates.too_many_upload_failures
else
if myTrack
if clientInfo?
if clientInfo.local_state == 'HQ'
uploadStateMsg = 'PENDING UPLOAD'
uploadStateClass = 'upload-soon'
uploadState = @uploadStates.me_upload_soon
else
uploadStateMsg = 'MISSING'
uploadStateClass = 'missing'
uploadState = @uploadStates.missing
else
uploadStateMsg = 'MISSING'
uploadStateClass = 'missing'
uploadState = @uploadStates.missing
else
uploadStateMsg = 'PENDING UPLOAD'
uploadStateClass = 'upload-soon'
uploadState = @uploadStates.them_upload_soon
else
uploadStateMsg = 'UPLOADED'
uploadStateClass = 'uploaded'
if myTrack
uploadState = @uploadStates.me_uploaded
else
uploadState = @uploadStates.them_uploaded
else
uploadStateMsg = 'NOT MINE'
uploadStateClass = 'not_mine'
uploadState = @uploadStates.not_mine
$clientState = $track.find('.client-state')
$clientStateMsg = $clientState.find('.msg')
$clientStateProgress = $clientState.find('.progress')
$uploadState = $track.find('.upload-state')
$uploadStateMsg = $uploadState.find('.msg')
$uploadStateProgress = $uploadState.find('.progress')
$clientState.removeClass('discarded missing hq unknown error not-mine').addClass(clientStateClass).attr('data-state', clientState).data('custom-class', clientStateClass)
$clientStateMsg.text(clientStateMsg)
$clientStateProgress.css('width', '0')
$uploadState.removeClass('upload-soon error unknown missing uploaded not-mine').addClass(uploadStateClass).attr('data-state', uploadState).data('custom-class', uploadStateClass)
$uploadStateMsg.text(uploadStateMsg)
$uploadStateProgress.css('width', '0')
# this allows us to make styling decisions based on the combination of both client and upload state.
$track.addClass("clientState-#{clientStateClass}").addClass("uploadState-#{uploadStateClass}")
$clientRetry = $clientState.find('.retry')
$uploadRetry = $uploadState.find('.retry')
if gon.isNativeClient
# handle client state
# only show RETRY button if you have a SQ or if it's missing, and it's been uploaded already
if (clientState == @clientStates.missing) and (uploadState == @uploadStates.me_uploaded or uploadState == @uploadStates.them_uploaded)
$clientRetry.show()
else
$clientRetry.hide()
# only show RETRY button if you have the HQ track, it's your track, and the server doesn't yet have it
if myTrack and @clientStates.hq and (uploadState == @uploadStates.error or uploadState == @uploadStates.me_upload_soon)
$uploadRetry.show()
else
$uploadRetry.hide()
else
$clientRetry.hide()
$uploadRetry.hide()
associateClientInfo: (recording) =>
for clientInfo in recording.local_tracks
$track = @list.find(".recorded-track[data-recording-id='#{recording.recording_id}'][data-client-track-id='#{clientInfo.client_track_id}']")
$track.data('client-info', clientInfo)
$track.data('total-size', recording.size)
for clientInfo in recording.backing_tracks
$track = @list.find(".recorded-backing-track[data-recording-id='#{recording.recording_id}'][data-client-track-id='#{clientInfo.client_track_id}']")
$track.data('client-info', clientInfo)
$track.data('total-size', recording.size)
$track = @list.find(".mix[data-recording-id='#{recording.recording_id}']")
$track.data('client-info', recording.mix)
$track.data('total-size', recording.size)
@ -457,11 +588,77 @@ context.JK.SyncViewer = class SyncViewer
uploadStateClass: uploadStateClass}
{variable: 'data'})
onHoverOfStateIndicator: () ->
displayBackingTrackHover: ($recordedTrack) =>
$clientState = $recordedTrack.find('.client-state')
$clientStateMsg = $clientState.find('.msg')
clientStateClass = $clientState.data('custom-class')
clientState = $clientState.attr('data-state')
clientInfo = $recordedTrack.data('client-info')
$uploadState = $recordedTrack.find('.upload-state')
$uploadStateMsg = $uploadState.find('.msg')
uploadStateClass = $uploadState.data('custom-class')
uploadState = $uploadState.attr('data-state')
serverInfo = $recordedTrack.data('server-info')
# decide on special case strings first
summary = ''
if clientState == @clientStates.not_mine && @uploadStates.them_uploaded
# this is not our backing track
summary = "#{serverInfo.user.name} opened this backing track. Due to legal concerns, we can not distribute it to you."
else if clientState == @clientStates.not_mine && @uploadStates.them_upload_soon
# this is not our backing track
summary = "#{serverInfo.user.name} has not yet uploaded their backing track."
else if clientState == @clientStates.missing && uploadState == @uploadStates.me_uploaded
# we have no version of the track at all, and the other user has uploaded the HQ version... it's coming soon!
summary = "You have previously uploaded the high-quality version of this track. JamKazam will soon restore it and then this backing track will no longer be missing."
else if clientState == @clientStates.discarded && (uploadState == @uploadStates.me_uploaded or uploadState == @uploadStates.them_uploaded)
# we decided not to keep the recording... so it's important to clarify why they are seeing it at all
summary = "When this recording was made, you elected to not keep it. JamKazam already uploaded your high-quality backing track for the recording, because at least one other person decided to keep the recording and needs your backing track to make a high-quality mix."
else if clientState == @clientStates.discarded
# we decided not to keep the recording... so it's important to clarify why they are seeing it at all
summary = "When this recording was made, you elected to not keep it. JamKazam will still try to upload your high-quality backing track for the recording, because at least one other person decided to keep the recording and needs your backing track to make a high-quality mix."
else if clientState == @clientStates.hq and ( uploadState == @uploadStates.me_uploaded )
summary = "Both you and the JamKazam server have the high-quality version of this track. Once all the other tracks for this recording are also synchronized, then the final mix can be made."
clientStateDefinition = switch clientState
when @clientStates.too_many_downloads then "This backing track has been downloaded an unusually large number of times. No more downloads are allowed."
when @clientStates.hq then "HIGHEST QUALITY means you have the original version of this backing track."
when @clientStates.missing then "MISSING means you do not have this backing track anymore."
when @clientStates.discarded then "DISCARDED means you chose to not keep this recording when the recording was over."
when @clientStates.not_mine then "NOT MINE means someone else opened and played this backing track."
else 'There is no help for this state'
uploadStateDefinition = switch uploadState
when @uploadStates.too_many_upload_failures then "Failed attempts at uploading this backing track has happened an unusually large times. No more uploads will be attempted."
when @uploadStates.me_upload_soon then "PENDING UPLOAD means your JamKazam application will upload this backing track soon."
when @uploadStates.them_up_soon then "PENDING UPLOAD means #{serverInfo.user.name} will upload this backing track soon."
when @uploadStates.me_uploaded then "UPLOADED means you have already uploaded this backing track."
when @uploadStates.them_uploaded then "UPLOADED means #{serverInfo.user.name} has already uploaded this backing track."
when @uploadStates.missing then "MISSING means your JamKazam application does not have this backing track, and the server does not either."
when @uploadStates.not_mine then "NOT MINE means someone else opened and played this backing track."
context._.template(@templateHoverRecordedBackingTrack.html(),
{summary: summary,
clientStateDefinition: clientStateDefinition,
uploadStateDefinition: uploadStateDefinition,
clientStateMsg: $clientStateMsg.text(),
uploadStateMsg: $uploadStateMsg.text(),
clientStateClass: clientStateClass,
uploadStateClass: uploadStateClass}
{variable: 'data'})
onTrackHoverOfStateIndicator: () ->
$recordedTrack = $(this).closest('.recorded-track.sync')
self = $recordedTrack.data('sync-viewer')
self.displayTrackHover($recordedTrack)
onBackingTrackHoverOfStateIndicator: () ->
$recordedTrack = $(this).closest('.recorded-backing-track.sync')
self = $recordedTrack.data('sync-viewer')
self.displayBackingTrackHover($recordedTrack)
onStreamMixHover: () ->
$streamMix = $(this).closest('.stream-mix.sync')
self = $streamMix.data('sync-viewer')
@ -512,6 +709,39 @@ context.JK.SyncViewer = class SyncViewer
return false
retryDownloadRecordedBackingTrack: (e) =>
$retry = $(e.target)
$track = $retry.closest('.recorded-backing-track')
serverInfo = $track.data('server-info')
console.log("track serverInfo", $track, serverInfo)
this.sendCommand($retry, {
type: 'recorded_backing_track',
action: 'download'
queue: 'download',
recording_id: serverInfo.recording_id
track_id: serverInfo.client_track_id
})
return false
retryUploadRecordedBackingTrack: (e) =>
$retry = $(e.target)
$track = $retry.closest('.recorded-backing-track')
serverInfo = $track.data('server-info')
console.log("track serverInfo", $track, serverInfo)
this.sendCommand($retry, {
type: 'recorded_backing_track',
action: 'upload'
queue: 'upload',
recording_id: serverInfo.recording_id
track_id: serverInfo.client_track_id
})
return false
createMix: (userSync) =>
recordingInfo = null
if userSync == 'fake'
@ -548,8 +778,26 @@ context.JK.SyncViewer = class SyncViewer
$uploadStateRetry.click(this.retryUploadRecordedTrack)
context.JK.bindHoverEvents($track)
context.JK.bindInstrumentHover($track, {positions:['top'], shrinkToFit: true});
context.JK.hoverBubble($clientState, this.onHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']})
context.JK.hoverBubble($uploadState, this.onHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']})
context.JK.hoverBubble($clientState, this.onTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']})
context.JK.hoverBubble($uploadState, this.onTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']})
$clientState.addClass('is-native-client') if gon.isNativeClient
$uploadState.addClass('is-native-client') if gon.isNativeClient
$track
createBackingTrack: (userSync) =>
$track = $(context._.template(@templateRecordedBackingTrack.html(), userSync, {variable: 'data'}))
$track.data('server-info', userSync)
$track.data('sync-viewer', this)
$clientState = $track.find('.client-state')
$uploadState = $track.find('.upload-state')
$clientStateRetry = $clientState.find('.retry')
$clientStateRetry.click(this.retryDownloadRecordedBackingTrack)
$uploadStateRetry = $uploadState.find('.retry')
$uploadStateRetry.click(this.retryUploadRecordedBackingTrack)
context.JK.bindHoverEvents($track)
context.JK.bindInstrumentHover($track, {positions:['top'], shrinkToFit: true});
context.JK.hoverBubble($clientState, this.onBackingTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']})
context.JK.hoverBubble($uploadState, this.onBackingTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']})
$clientState.addClass('is-native-client') if gon.isNativeClient
$uploadState.addClass('is-native-client') if gon.isNativeClient
$track
@ -687,6 +935,8 @@ context.JK.SyncViewer = class SyncViewer
for userSync in response.entries
if userSync.type == 'recorded_track'
@list.append(this.createTrack(userSync))
if userSync.type == 'recorded_backing_track'
@list.append(this.createBackingTrack(userSync))
else if userSync.type == 'mix'
@list.append(this.createMix(userSync))
else if userSync.type == 'stream_mix'
@ -707,6 +957,8 @@ context.JK.SyncViewer = class SyncViewer
for track in @list.find('.recorded-track.sync')
this.updateTrackState($(track))
for track in @list.find('.recorded-backing-track.sync')
this.updateBackingTrackState($(track))
for streamMix in @list.find('.stream-mix.sync')
this.updateStreamMixState($(streamMix))
@ -726,6 +978,18 @@ context.JK.SyncViewer = class SyncViewer
deferred.resolve(matchingTrack.data('server-info'))
return deferred
resolveBackingTrack: (commandMetadata) =>
recordingId = commandMetadata['recording_id']
clientTrackId = commandMetadata['track_id']
matchingTrack = @list.find(".recorded-backing-track[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
if matchingTrack.length == 0
return @rest.getRecordedBackingTrack({recording_id: recordingId, track_id: clientTrackId})
else
deferred = $.Deferred();
deferred.resolve(matchingTrack.data('server-info'))
return deferred
renderFullUploadRecordedTrack: (serverInfo) =>
$track = $(context._.template(@templateRecordedTrackCommand.html(), $.extend(serverInfo, {action:'UPLOADING'}), {variable: 'data'}))
$busy = @uploadProgress.find('.busy')
@ -738,6 +1002,18 @@ context.JK.SyncViewer = class SyncViewer
$busy.empty().append($track)
@downloadProgress.find('.progress').css('width', '0%')
renderFullUploadRecordedBackingTrack: (serverInfo) =>
$track = $(context._.template(@templateRecordedBackingTrackCommand.html(), $.extend(serverInfo, {action:'UPLOADING'}), {variable: 'data'}))
$busy = @uploadProgress.find('.busy')
$busy.empty().append($track)
@uploadProgress.find('.progress').css('width', '0%')
renderFullDownloadRecordedBackingTrack: (serverInfo) =>
$track = $(context._.template(@templateRecordedBackingTrackCommand.html(), $.extend(serverInfo, {action:'DOWNLOADING'}), {variable: 'data'}))
$busy = @downloadProgress.find('.busy')
$busy.empty().append($track)
@downloadProgress.find('.progress').css('width', '0%')
# this will either show a generic placeholder, or immediately show the whole track
renderDownloadRecordedTrack: (commandId, commandMetadata) =>
# try to find the info in the list; if we can't find it, then resolve it
@ -756,6 +1032,23 @@ context.JK.SyncViewer = class SyncViewer
deferred.done(this.renderFullUploadRecordedTrack).fail(()=> @logger.error("unable to fetch recorded_track info") )
# this will either show a generic placeholder, or immediately show the whole track
renderDownloadRecordedBackingTrack: (commandId, commandMetadata) =>
# try to find the info in the list; if we can't find it, then resolve it
deferred = this.resolveBackingTrack(commandMetadata)
if deferred.state() == 'pending'
this.renderGeneric(commandId, 'download', commandMetadata)
deferred.done(this.renderFullDownloadRecordedBackingTrack).fail(()=> @logger.error("unable to fetch recorded_backing_track info") )
renderUploadRecordedBackingTrack: (commandId, commandMetadata) =>
# try to find the info in the list; if we can't find it, then resolve it
deferred = this.resolveBackingTrack(commandMetadata)
if deferred.state() == 'pending'
this.renderGeneric(commandId, 'upload', commandMetadata)
deferred.done(this.renderFullUploadRecordedBackingTrack).fail(()=> @logger.error("unable to fetch recorded_backing_track info") )
renderGeneric: (commandId, category, commandMetadata) =>
commandMetadata.displayType = this.displayName(commandMetadata)
@ -794,6 +1087,8 @@ context.JK.SyncViewer = class SyncViewer
@downloadProgress.addClass('busy')
if commandMetadata.type == 'recorded_track' and commandMetadata.action == 'download'
this.renderDownloadRecordedTrack(commandId, commandMetadata)
else if commandMetadata.type == 'recorded_backing_track' and commandMetadata.action == 'download'
this.renderDownloadRecordedBackingTrack(commandId, commandMetadata)
else
this.renderGeneric(commandId, 'download', commandMetadata)
else if commandMetadata.queue == 'upload'
@ -803,6 +1098,8 @@ context.JK.SyncViewer = class SyncViewer
@uploadProgress.addClass('busy')
if commandMetadata.type == 'recorded_track' and commandMetadata.action == 'upload'
this.renderUploadRecordedTrack(commandId, commandMetadata)
else if commandMetadata.type == 'recorded_backing_track' and commandMetadata.action == 'upload'
this.renderUploadRecordedBackingTrack(commandId, commandMetadata)
else
this.renderGeneric(commandId, 'upload', commandMetadata)
else if commandMetadata.queue == 'cleanup'
@ -820,6 +1117,12 @@ context.JK.SyncViewer = class SyncViewer
$track.data('server-info', userSync)
this.associateClientInfo(clientRecordings.recordings[0])
this.updateTrackState($track)
else if userSync.type == 'recorded_backing_track'
$track = @list.find(".sync[data-id='#{userSync.id}']")
continue if $track.length == 0
$track.data('server-info', userSync)
this.associateClientInfo(clientRecordings.recordings[0])
this.updateBackingTrackState($track)
else if userSync.type == 'mix'
# check if there is a virtual mix 1st; if so, update it
$mix = @list.find(".mix.virtual[data-recording-id='#{userSync.recording.id}']")
@ -839,20 +1142,6 @@ context.JK.SyncViewer = class SyncViewer
updateSingleRecording: (recording_id) =>
@rest.getUserSyncs({recording_id: recording_id}).done(this.renderSingleRecording)
updateSingleRecordedTrack: ($track) =>
serverInfo = $track.data('server-info')
@rest.getUserSync({user_sync_id: serverInfo.id})
.done((userSync) =>
# associate new server-info with this track
$track.data('server-info', userSync)
# associate new client-info with this track
clientRecordings = context.jamClient.GetLocalRecordingState(recordings: [userSync.recording])
this.associateClientInfo(clientRecordings.recordings[0])
this.updateTrackState($track)
)
.fail(@app.ajaxError)
updateProgressOnSync: ($track, queue, percentage) =>
state = if queue == 'upload' then '.upload-state' else '.client-state'
$progress = $track.find("#{state} .progress")
@ -892,10 +1181,10 @@ context.JK.SyncViewer = class SyncViewer
$progress = @downloadProgress.find('.progress')
$progress.css('width', percentage + '%')
if @downloadMetadata.type == 'recorded_track'
if @downloadMetadata.type == 'recorded_track' or @downloadMetadata.type == 'recorded_backing_track'
clientTrackId = @downloadMetadata['track_id']
recordingId = @downloadMetadata['recording_id']
$matchingTrack = @list.find(".recorded-track.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
$matchingTrack = @list.find(".track-item.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
if $matchingTrack.length > 0
this.updateProgressOnSync($matchingTrack, 'download', percentage)
@ -903,10 +1192,10 @@ context.JK.SyncViewer = class SyncViewer
$progress = @uploadProgress.find('.progress')
$progress.css('width', percentage + '%')
if @uploadMetadata.type == 'recorded_track' and @uploadMetadata.action == 'upload'
if (@uploadMetadata.type == 'recorded_track' or @uploadMetadata.type == 'recorded_backing_track') and @uploadMetadata.action == 'upload'
clientTrackId = @uploadMetadata['track_id']
recordingId = @uploadMetadata['recording_id']
$matchingTrack = @list.find(".recorded-track.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
$matchingTrack = @list.find(".track-item.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
if $matchingTrack.length > 0
this.updateProgressOnSync($matchingTrack, 'upload', percentage)
else if @uploadMetadata.type == 'stream_mix' and @uploadMetadata.action == 'upload'
@ -977,15 +1266,15 @@ context.JK.SyncViewer = class SyncViewer
this.logResult(data.commandMetadata, false, data.commandReason, true)
displayName: (metadata) =>
if metadata.type == 'recorded_track' && metadata.action == 'download'
if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'download'
return 'DOWNLOADING TRACK'
else if metadata.type == 'recorded_track' && metadata.action == 'upload'
else if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'upload'
return 'UPLOADING TRACK'
else if metadata.type == 'mix' && metadata.action == 'download'
return 'DOWNLOADING MIX'
else if metadata.type == 'recorded_track' && metadata.action == 'convert'
else if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'convert'
return 'COMPRESSING TRACK'
else if metadata.type == 'recorded_track' && metadata.action == 'delete'
else if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'delete'
return 'CLEANUP TRACK'
else if metadata.type == 'stream_mix' && metadata.action == 'upload'
return 'UPLOADING STREAM MIX'

View File

@ -30,6 +30,28 @@
return tracks;
},
getBackingTracks: function(jamClient) {
var mediaTracks = context.JK.TrackHelpers.getTracks(jamClient, 4);
console.log("mediaTracks", mediaTracks)
var backingTracks = []
context._.each(mediaTracks, function(mediaTrack) {
// the check for 'not managed' means this is not a track opened by a recording, basically
// we do not try and sync these sorts of backing tracks to the server, because they
// are already encompassed by
if(mediaTrack.media_type == "BackingTrack" && !mediaTrack.managed) {
var track = {};
track.client_track_id = mediaTrack.persisted_track_id;
track.client_resource_id = mediaTrack.rid;
track.filename = mediaTrack.filename;
backingTracks.push(track);
}
})
return backingTracks;
},
/**
* This function resolves which tracks to configure for a user
* when creating or joining a session. By default, tracks are pulled

View File

@ -987,6 +987,15 @@
return hasFlash;
}
context.JK.getNameOfFile = function(filename) {
var index = filename.lastIndexOf('/');
if(index == -1) {
index = filename.lastIndexOf('\\');
}
return index == -1 ? filename : filename.substring(index + 1, filename.length)
}
context.JK.hasOneConfiguredDevice = function () {
var result = context.jamClient.FTUEGetGoodConfigurationList();
logger.debug("hasOneConfiguredDevice: ", result);

View File

@ -208,6 +208,10 @@ $fair: #cc9900;
background-color: $error;
}
&.not_mine {
background-color: $good;
}
&.discarded {
background-color: $unknown;
}
@ -252,6 +256,10 @@ $fair: #cc9900;
background-color: $good;
}
&.not_mine {
background-color: $good;
}
.retry {
display:none;
position:absolute;

View File

@ -45,7 +45,7 @@ body.jam, body.web, .dialog{
}
}
.help-hover-recorded-tracks, .help-hover-stream-mix {
.help-hover-recorded-tracks, .help-hover-stream-mix, .help-hover-recorded-backing-tracks {
font-size:12px;
padding:5px;

View File

@ -18,7 +18,7 @@
.track {
width:70px;
height:290px;
height:300px;
display:inline-block;
margin-right:8px;
position:relative;
@ -50,6 +50,9 @@
vertical-align:top;
}
.session-recordedtracks-container {
//display: block;
}
.recording-controls {
display:none;
@ -74,17 +77,23 @@
left:5px;
}
.open-media-file-header {
.open-media-file-header, .use-metronome-header {
font-size:16px;
line-height:100%;
margin:0;
float:left;
img {
position:relative;
top:3px;
}
}
.open-media-file-header {
float: left;
}
.use-metronome-header {
clear: both;
}
.open-media-file-options {
font-size:16px;
@ -110,8 +119,21 @@
.session-recording-name-wrapper{
position:relative;
white-space:nowrap;
display:none;
white-space:normal;
display:none;
.session-recording-name {
position:relative;
margin-top:9px;
margin-bottom:8px;
font-size:16px;
height: 22px;
min-height: 22px;
max-height: 22px;
display: inline-block;
width:60%;
text-overflow:ellipsis;
}
.session-add {
margin-top:9px;
@ -126,13 +148,6 @@
}
}
.session-recording-name {
width:60%;
overflow:hidden;
margin-top:9px;
margin-bottom:8px;
font-size:16px;
}
}
@ -201,6 +216,9 @@ table.vu td {
position: absolute;
text-align:center;
width: 55px;
height: 15px;
min-height: 11px;
max-height: 33px;
max-width: 55px;
white-space:normal;
top: 3px;
@ -208,6 +226,7 @@ table.vu td {
font-family: Arial, Helvetica, sans-serif;
font-size: 11px;
font-weight: bold;
text-overflow:ellipsis;
}
.track-close {
@ -319,8 +338,6 @@ table.vu td {
color: inherit;
}
.session-add {
margin-top:9px;
margin-bottom:8px;
@ -345,7 +362,7 @@ table.vu td {
overflow-x:auto;
overflow-y:hidden;
width:100%;
height:340px;
height:370px;
float:left;
white-space:nowrap;
}
@ -482,7 +499,7 @@ table.vu td {
.track-gain {
position:absolute;
width:28px;
height:83px;
height:63px;
top:138px;
left:23px;
background-image:url('/assets/content/bkg_gain_slider.png');
@ -514,6 +531,45 @@ table.vu td {
height: 18px;
background-image:url('/assets/content/icon_mute.png');
background-repeat:no-repeat;
text-align: center;
}
.track-icon-loop {
cursor: pointer;
position:absolute;
top:250px;
left:11px;
width: 20px;
height: 18px;
text-align: center;
font-size: 8pt;
font-weight: bold;
.icheckbox_minimal {
top:5px;
margin-right:5px;
}
}
.metronome-selects {
position: absolute;
width: 52px;
top:252px;
left: 10px;
height: 18px;
text-align: center;
//display: block;
//padding: 4px;
select.metronome-select {
position: relative;
padding: 4px 0px 4px 0px;
margin: 0;
width: 100% !important;
font-size: 10px;
font-weight: normal;
}
}
.track-icon-mute.muted {
@ -524,12 +580,12 @@ table.vu td {
}
.session-livetracks .track-icon-mute, .session-recordings .track-icon-mute {
top:245px;
top:225px;
}
.track-icon-settings {
position:absolute;
top:255px;
top:235px;
left:28px;
}

View File

@ -1,7 +1,7 @@
@import "client/common";
table.findsession-table, table.local-recordings, table.open-jam-tracks, #account-session-detail {
table.findsession-table, table.local-recordings, table.open-jam-tracks, table.open-backing-tracks, #account-session-detail {
.latency-unacceptable {
width: 50px;
@ -64,7 +64,7 @@ table.findsession-table, table.local-recordings, table.open-jam-tracks, #account
text-align:center;
}
}
table.findsession-table, table.local-recordings, table.open-jam-tracks {
table.findsession-table, table.local-recordings, table.open-jam-tracks, table.open-backing-tracks {
width:98%;
height:10%;
font-size:11px;

View File

@ -0,0 +1,40 @@
@import "client/common";
#open-backing-track-dialog {
table.open-backing-tracks {
tbody {
tr:hover {
background-color: #777;
cursor:pointer;
}
tr[data-local-state=MISSING], tr[data-local-state=PARTIALLY_MISSING] {
background-color:#777;
color:#aaa;
}
}
}
.right {
margin-right:10px;
}
.help-links {
text-align: left;
position: absolute;
margin: 0 auto;
width: 70%;
//left: 15%;
font-size: 12px;
padding-top:5px;
a {
margin:0 10px;
}
}
.paginator-holder {
padding-top:3px;
}
}

View File

@ -0,0 +1,32 @@
class ApiBackingTracksController < ApiController
# have to be signed in currently to see this screen
before_filter :api_signed_in_user
before_filter :lookup_recorded_backing_track, :only => [ :backing_track_silent ]
respond_to :json
def index
tracks = [
{:name=>'foo',:path=>"foobar.mp3", :length=>4283},
{:name=>'bar',:path=>"foo.mp3",:length=>3257}
]
@backing_tracks, @next = tracks, nil
render "api_backing_tracks/index", :layout => nil
end
def backing_track_silent
@recorded_backing_track.mark_silent
render :json => {}, :status => 200
end
private
def lookup_recorded_backing_track
@recorded_backing_track = RecordedBackingTrack.find_by_recording_id_and_client_track_id!(params[:id], params[:track_id])
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_backing_track.recording.has_access?(current_user)
end
end # class ApiBackingTracksController

View File

@ -4,7 +4,7 @@ class ApiMusicSessionsController < ApiController
# have to be signed in currently to see this screen
before_filter :api_signed_in_user, :except => [ :add_like, :show, :show_history, :add_session_info_comment ]
before_filter :lookup_session, only: [:show, :update, :delete, :claimed_recording_start, :claimed_recording_stop, :track_sync, :jam_track_open, :jam_track_close]
before_filter :lookup_session, only: [:show, :update, :delete, :claimed_recording_start, :claimed_recording_stop, :track_sync, :jam_track_open, :jam_track_close, :backing_track_open, :backing_track_close, :metronome_open, :metronome_close]
skip_before_filter :api_signed_in_user, only: [:perf_upload]
respond_to :json
@ -357,7 +357,7 @@ class ApiMusicSessionsController < ApiController
end
def track_sync
@tracks = MusicSessionManager.new.sync_tracks(@music_session, params[:client_id], params[:tracks])
@tracks = MusicSessionManager.new.sync_tracks(@music_session, params[:client_id], params[:tracks], params[:backing_tracks])
unless @tracks.kind_of? Array
# we have to do this because api_session_detail_url will fail with a bad @tracks
@ -597,8 +597,44 @@ class ApiMusicSessionsController < ApiController
respond_with_model(@music_session)
end
def backing_track_open
unless @music_session.users.exists?(current_user)
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
end
private
@backing_track_path = params[:backing_track_path]
@music_session.open_backing_track(current_user, @backing_track_path)
respond_with_model(@music_session)
end
def backing_track_close
unless @music_session.users.exists?(current_user)
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
end
@music_session.close_backing_track()
respond_with_model(@music_session)
end
def metronome_open
unless @music_session.users.exists?(current_user)
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
end
@music_session.open_metronome(current_user)
respond_with_model(@music_session)
end
def metronome_close
unless @music_session.users.exists?(current_user)
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
end
@music_session.close_metronome()
respond_with_model(@music_session)
end
private
def lookup_session
@music_session = ActiveMusicSession.find(params[:id])

View File

@ -3,6 +3,7 @@ class ApiRecordingsController < ApiController
before_filter :lookup_recording, :only => [ :show, :stop, :claim, :discard, :keep, :delete_claim ]
before_filter :lookup_recorded_track, :only => [ :download, :upload_next_part, :upload_sign, :upload_part_complete, :upload_complete ]
before_filter :lookup_recorded_backing_track, :only => [ :backing_track_download, :backing_track_upload_next_part, :backing_track_upload_sign, :backing_track_upload_part_complete, :backing_track_upload_complete ]
before_filter :lookup_recorded_video, :only => [ :video_upload_sign, :video_upload_start, :video_upload_complete ]
before_filter :lookup_stream_mix, :only => [ :upload_next_part_stream_mix, :upload_sign_stream_mix, :upload_part_complete_stream_mix, :upload_complete_stream_mix ]
@ -43,7 +44,11 @@ class ApiRecordingsController < ApiController
@recorded_track = RecordedTrack.find_by_recording_id_and_client_track_id(params[:id], params[:track_id])
end
def download
def show_recorded_backing_track
@recorded_backing_track = RecordedBackingTrack.find_by_recording_id_and_client_track_id(params[:id], params[:track_id])
end
def download # track
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_track.can_download?(current_user)
@recorded_track.current_user = current_user
@ -58,6 +63,21 @@ class ApiRecordingsController < ApiController
end
end
def backing_track_download
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_backing_track.can_download?(current_user)
@recorded_backing_track.current_user = current_user
@recorded_backing_track.update_download_count
@recorded_backing_track.valid?
if !@recorded_backing_track.errors.any?
@recorded_backing_track.save!
redirect_to @recorded_backing_track.sign_url
else
render :json => { :message => "download limit surpassed" }, :status => 404
end
end
def start
music_session = ActiveMusicSession.find(params[:music_session_id])
@ -227,6 +247,61 @@ class ApiRecordingsController < ApiController
end
end
def backing_track_upload_next_part
length = params[:length]
md5 = params[:md5]
@recorded_backing_track.upload_next_part(length, md5)
if @recorded_backing_track.errors.any?
response.status = :unprocessable_entity
# this is not typical, but please don't change this line unless you are sure it won't break anything
# this is needed because after_rollback in the RecordedTrackObserver touches the model and something about it's
# state doesn't cause errors to shoot out like normal.
render :json => { :errors => @recorded_backing_track.errors }, :status => 422
else
result = {
:part => @recorded_backing_track.next_part_to_upload,
:offset => @recorded_backing_track.file_offset.to_s
}
render :json => result, :status => 200
end
end
def backing_track_upload_sign
render :json => @recorded_backing_track.upload_sign(params[:md5]), :status => 200
end
def backing_track_upload_part_complete
part = params[:part]
offset = params[:offset]
@recorded_backing_track.upload_part_complete(part, offset)
if @recorded_backing_track.errors.any?
response.status = :unprocessable_entity
respond_with @recorded_backing_track
else
render :json => {}, :status => 200
end
end
def backing_track_upload_complete
@recorded_backing_track.upload_complete
@recorded_backing_track.recording.upload_complete
if @recorded_backing_track.errors.any?
response.status = :unprocessable_entity
respond_with @recorded_backing_track
return
else
render :json => {}, :status => 200
end
end
# POST /api/recordings/:id/videos/:video_id/upload_sign
def video_upload_sign
length = params[:length]
@ -314,6 +389,11 @@ class ApiRecordingsController < ApiController
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_track.recording.has_access?(current_user)
end
def lookup_recorded_backing_track
@recorded_backing_track = RecordedBackingTrack.find_by_recording_id_and_client_track_id!(params[:id], params[:track_id])
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_backing_track.recording.has_access?(current_user)
end
def lookup_stream_mix
@quick_mix = QuickMix.find_by_recording_id_and_user_id!(params[:id], current_user.id)
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @quick_mix.recording.has_access?(current_user)

View File

@ -91,4 +91,10 @@ module SessionsHelper
current_user.musician? ? 'Musician' : 'Fan'
end
end
def metronome_tempos
[
40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 63, 66, 69, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 126, 132, 138, 144, 152, 160, 168, 176, 184, 192, 200, 208
]
end
end

View File

@ -0,0 +1,7 @@
node :next do |page|
@next
end
node :backing_tracks do |page|
@backing_tracks
end

View File

@ -2,6 +2,8 @@
# I don't think I need to include URLs since that's handled by syncing. This is just to make the metadata
# depictable.
# THIS IS USED DIRECTLY BY THE CLIENT. DO NOT CHANGE FORMAT UNLESS YOU VERIFY CLIENT FIRST. IN PARTICULAR RecordingFileStorage#getLocalRecordingState
object @claimed_recording
attributes :id, :name, :description, :is_public, :genre_id, :discarded
@ -36,6 +38,18 @@ child(:recording => :recording) {
}
}
child(:recorded_backing_tracks => :recorded_backing_tracks) {
attributes :id, :fully_uploaded, :client_track_id, :client_id, :filename
child(:user => :user) {
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :photo_url
}
node :mine do |recorded_backing_track|
recorded_backing_track.user == current_user
end
}
child(:comments => :comments) {
attributes :comment, :created_at

View File

@ -13,7 +13,7 @@ if !current_user
}
else
attributes :id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id, :track_changes_counter, :max_score
attributes :id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id, :track_changes_counter, :max_score, :backing_track_path, :metronome_active
node :can_join do |session|
session.can_join?(current_user, true)
@ -54,6 +54,10 @@ else
child(:tracks => :tracks) {
attributes :id, :connection_id, :instrument_id, :sound, :client_track_id, :client_resource_id, :updated_at
}
child(:backing_tracks => :backing_tracks) {
attributes :id, :connection_id, :filename, :client_track_id, :client_resource_id, :updated_at
}
}
child({:invitations => :invitations}) {
@ -114,6 +118,14 @@ else
attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url
}
}
child(:recorded_backing_tracks => :recorded_backing_tracks) {
attributes :id, :fully_uploaded, :client_track_id, :client_id, :filename
child(:user => :user) {
attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url
}
}
}
}

View File

@ -27,6 +27,12 @@ child(:recorded_tracks => :recorded_tracks) {
end
}
child(:recorded_backing_tracks => :recorded_backing_tracks) {
node do |recorded_backing_track|
partial("api_recordings/show_recorded_backing_track", :object => recorded_backing_track)
end
}
child(:comments => :comments) {
attributes :comment, :created_at

View File

@ -0,0 +1,11 @@
object @recorded_backing_track
attributes :id, :fully_uploaded, :client_track_id, :client_id, :recording_id, :filename
node :mine do |recorded_backing_track|
recorded_backing_track.user == current_user
end
child(:user => :user) {
attributes :id, :first_name, :last_name, :city, :state, :country, :location, :photo_url
}

View File

@ -18,7 +18,6 @@ glue :recorded_track do
partial("api_recordings/show", :object => recorded_track.recording)
end
node :upload do |recorded_track|
{
should_upload: true,
@ -35,6 +34,45 @@ glue :recorded_track do
end
glue :recorded_backing_track do
@object.current_user = current_user
node :type do |i|
'recorded_backing_track'
end
attributes :id, :recording_id, :client_id, :track_id, :client_track_id, :md5, :length, :download_count, :fully_uploaded, :upload_failures, :part_failures, :created_at, :filename
node :user do |recorded_backing_track|
partial("api_users/show_minimal", :object => recorded_backing_track.user)
end
node :recording do |recorded_backing_track|
partial("api_recordings/show", :object => recorded_backing_track.recording)
end
node :mine do |recorded_backing_track|
recorded_backing_track.user == current_user
end
node :upload do |recorded_backing_track|
{
should_upload: true,
too_many_upload_failures: recorded_backing_track.too_many_upload_failures?
}
end
node :download do |recorded_backing_track|
{
should_download: recorded_backing_track.can_download?(current_user),
too_many_downloads: recorded_backing_track.too_many_downloads?
}
end
end
glue :mix do
@object.current_user = current_user

View File

@ -1,199 +0,0 @@
<!-- Actual Session Screen -->
<div layout="screen" layout-id="session" layout-arg="id" class="screen secondary" id="session-screen">
<div class="content-head">
<div class="content-icon">
<%= image_tag "shared/icon_session.png", {:height => 19, :width => 19} %>
</div>
<h1>session</h1>
</div>
<div class="content-body">
<!-- session controls -->
<div id="session-controls">
<a class="button-grey resync left" id="session-resync">
<%= image_tag "content/icon_resync.png", {:align => "texttop", :height => 14, :width => 12} %>
RESYNC
</a>
<a class="button-grey left" layout-link="session-settings" id="session-settings-button">
<%= image_tag "content/icon_settings_sm.png", {:align => "texttop", :height => 12, :width => 12} %>
SETTINGS
</a>
<a layout-link="share-dialog" class="button-grey left">
<%= image_tag "content/icon_share.png", {:align => "texttop", :height => 12, :width => 12} %>
SHARE
</a>
<!-- Volume Slider -->
<div class="block">
<div class="label">VOLUME:</div>
<div id="volume" class="fader lohi" mixer-id=""></div>
</div>
<!-- Mix: Me versus Others -->
<div class="block monitor-mode-holder">
<div class="label">MIX:</div>
<select class="monitor-mode easydropdown">
<option value="personal" class="label">Personal</option>
<option value="master">Master</option>
</select>
</div>
<!--
<div class="block">
<div class="label">MONITOR:</div>
<div class="label"><small>others</small></div>
<div id="l2m" class="fader flat" mixer-id="__L2M__"></div>
<div class="label"><small>me</small></div>
</div>
-->
<!-- Leave Button -->
<a class="button-grey right leave" href="/client#/home" id="session-leave">X&nbsp;&nbsp;LEAVE</a>
</div>
<!-- end session controls -->
<!-- content scrolling area -->
<div id="tracks">
<div class="content-scroller">
<!-- content wrapper -->
<div class="content-wrapper">
<!-- my tracks -->
<div class="session-mytracks">
<h2>my tracks</h2>
<div id="track-settings" class="session-add" style="display:block;" layout-link="configure-tracks">
<%= image_tag "content/icon_settings_lg.png", {:width => 18, :height => 18} %>
<span>Settings</span>
</div>
<div class="session-tracks-scroller">
<div id="session-mytracks-container"></div>
<div id="voice-chat" class="voicechat" style="display:none;" mixer-id="">
<div class="voicechat-label">CHAT</div>
<div class="voicechat-gain"></div>
<div class="voicechat-mute enabled" control="mute" mixer-id=""></div>
</div>
</div>
</div>
<!-- live tracks -->
<div class="session-livetracks">
<h2>live tracks</h2>
<div class="session-add" layout-link="select-invites">
<a href="#" id="session-invite-musicians">
<%= image_tag "content/icon_add.png", {:width => 19, :height => 19, :align => "texttop"} %>&nbsp;&nbsp;Invite Musicians
</a>
</div>
<div class="session-tracks-scroller">
<div id="session-livetracks-container">
<div class="when-empty livetracks">
No other musicians <br/>
are in your session
</div>
</div>
<br clear="all" />
<div class="recording" id="recording-start-stop">
<a>
<%= image_tag "content/recordbutton-off.png", {:width => 20, :height => 20, :align => "absmiddle"} %>&nbsp;&nbsp;<span id="recording-status">Make a Recording</span>
</a>
</div>
</div>
</div>
<!-- recordings -->
<div class="session-recordings">
<h2>other audio</h2>
<div class="session-recording-name-wrapper">
<div class="session-recording-name left">(No recording loaded)</div>
<div class="session-add right">
<a id='close-playback-recording' href="#"><%= image_tag "content/icon_close.png", {:width => 18, :height => 20, :align => "texttop"} %>&nbsp;&nbsp;Close</a>
</div>
</div>
<div class="session-tracks-scroller">
<div id="session-recordedtracks-container">
<div class="when-empty recordings">
<span class="open-media-file-header"><%= image_tag "content/icon_folder.png", {width:22, height:20} %> Open:</span>
<ul class="open-media-file-options">
<li><a href="#" id="open-a-recording">Recording</a></li>
<% if Rails.application.config.jam_tracks_available %>
<li><a href="#" id="open-a-jamtrack">JamTrack</a></li>
<% end %>
<!--<li>Audio File</li>-->
</ul>
</div>
</div>
<br clear="all" />
<%= render "play_controls" %>
</div>
<!-- recording name and close button -->
<!--
<div class="session-recording-name-wrapper">
<div class="session-recording-name left">(No recording loaded)</div>
<div class="session-add right">
<a>
<%= image_tag "content/icon_close.png", {:width => 18, :height => 20, :align => "texttop"} %>&nbsp;&nbsp;Close
</a>
</div>
</div>
-->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- dialogs needed for Session screen (ORDER MATTERS) -->
<%= render "configureTrack" %>
<%= render "addTrack" %>
<%= render "addNewGear" %>
<%= render "error" %>
<%= render "sessionSettings" %>
<!-- Track Template -->
<script type="text/template" id="template-session-track">
<div track-id="{trackId}" class="session-track track" client-id="{clientId}">
<div class="track-vu-left" mixer-id="{vuMixerId}_vul"></div>
<div class="track-vu-right" mixer-id="{vuMixerId}_vur"></div>
<div class="track-label">{name}</div>
<div id="div-track-close" track-id="{trackId}" class="track-close op30">
<%= image_tag "content/icon_closetrack.png", {:width => 12, :height => 12} %>
</div>
<div class="{avatarClass}">
<img src="{avatar}"/>
</div>
<div class="track-instrument {preMasteredClass}">
<img src="{instrumentIcon}" width="45" height="45"/>
</div>
<div class="track-gain" mixer-id="{mixerId}"></div>
<!--
<div class="track-gain-wrapper"
control="fader" orientation="vertical">
<div class="track-gain-slider" style="bottom:{gainPercent}%;" control="fader-handle">
<%= image_tag "content/slider_gain_vertical.png", {:width => 28, :height => 11} %>
</div>
</div>
</div>
-->
<div class="track-icon-mute {muteClass}" control="mute" mixer-id="{muteMixerId}">
</div>
<!-- TODO - connection class from curly param -->
<div mixer-id="{mixerId}_connection" class="track-connection grey">CONNECTION</div>
<div class="disabled-track-overlay"></div>
</div>
</script>
<script type="text/template" id="template-option">
<option value="{value}" title="{label}" {selected}>{label}</option>
</script>
<!-- Genre option template -->
<script type="text/template" id="template-genre-option">
<option value="{value}">{label}</option>
</script>

View File

@ -0,0 +1,145 @@
#session-screen.screen.secondary[layout="screen" layout-id="session" layout-arg="id"]
.content-head
.content-icon
= image_tag "shared/icon_session.png", {:height => 19, :width => 19}
h1
| session
.content-body
#session-controls
a#session-resync.button-grey.resync.left
= image_tag "content/icon_resync.png", {:align => "texttop", :height => 14, :width => 12}
| RESYNC
a#session-settings-button.button-grey.left[layout-link="session-settings"]
= image_tag "content/icon_settings_sm.png", {:align => "texttop", :height => 12, :width => 12}
| SETTINGS
a.button-grey.left[layout-link="share-dialog"]
= image_tag "content/icon_share.png", {:align => "texttop", :height => 12, :width => 12}
| SHARE
.block
.label
| VOLUME:
#volume.fader.lohi[mixer-id=""]
.block.monitor-mode-holder
.label
| MIX:
select.monitor-mode.easydropdown
option.label[value="personal"]
| Personal
option[value="master"]
| Master
a#session-leave.button-grey.right.leave[href="/client#/home"]
| X  LEAVE
#tracks
.content-scroller
.content-wrapper
.session-mytracks
h2
| my tracks
#track-settings.session-add[style="display:block;" layout-link="configure-tracks"]
= image_tag "content/icon_settings_lg.png", {:width => 18, :height => 18}
span
| Settings
.session-tracks-scroller
#session-mytracks-container
#voice-chat.voicechat[style="display:none;" mixer-id=""]
.voicechat-label
| CHAT
.voicechat-gain
.voicechat-mute.enabled[control="mute" mixer-id=""]
.session-livetracks
h2
| live tracks
.session-add[layout-link="select-invites"]
a#session-invite-musicians[href="#"]
= image_tag "content/icon_add.png", {:width => 19, :height => 19, :align => "texttop"}
|   Invite Musicians
.session-tracks-scroller
#session-livetracks-container
.when-empty.livetracks
| No other musicians
br
| are in your session
br[clear="all"]
#recording-start-stop.recording
a
= image_tag "content/recordbutton-off.png", {:width => 20, :height => 20, :align => "absmiddle"}
|   
span#recording-status
| Make a Recording
.session-recordings
h2
| other audio
.session-recording-name-wrapper
.session-recording-name.left
| (No recording loaded)
.session-add.right
a#close-playback-recording[href="#"]
= image_tag "content/icon_close.png", {:width => 18, :height => 20, :align => "texttop"}
|   Close
.session-tracks-scroller
#session-recordedtracks-container
.when-empty.recordings
span.open-media-file-header
= image_tag "content/icon_folder.png", {width:22, height:20}
| Open:
ul.open-media-file-options
li
a#open-a-recording[href="#"]
| Recording
- if Rails.application.config.jam_tracks_available
li
a#open-a-jamtrack[href="#"]
| JamTrack
- if Rails.application.config.backing_tracks_available
li
a#open-a-backingtrack[href="#"]
| Audio File
.when-empty.use-metronome-header
- if Rails.application.config.metronome_available
= image_tag "content/icon_metronome.png", {width:22, height:20}
a#open-a-metronome[href="#"]
| Use Metronome
br[clear="all"]
= render "play_controls"
= render "configureTrack"
= render "addTrack"
= render "addNewGear"
= render "error"
= render "sessionSettings"
script#template-session-track[type="text/template"]
.session-track.track client-id="{clientId}" track-id="{trackId}"
.track-vu-left.mixer-id="{vuMixerId}_vul"
.track-vu-right.mixer-id="{vuMixerId}_vur"
.track-label[title="{name}"]
span.name-text="{name}"
#div-track-close.track-close.op30 track-id="{trackId}"
=image_tag("content/icon_closetrack.png", {width: 12, height: 12})
div class="{avatarClass}"
img src="{avatar}"
.track-instrument class="{preMasteredClass}"
img height="45" src="{instrumentIcon}" width="45"
.track-gain mixer-id="{mixerId}"
.track-icon-mute class="{muteClass}" control="mute" mixer-id="{muteMixerId}"
.track-icon-loop.hidden control="loop"
input#loop-button type="checkbox" value="loop" Loop
.track-connection.grey mixer-id="{mixerId}_connection"
CONNECTION
.disabled-track-overlay
.metronome-selects.hidden
select.metronome-select.metro-sound title="Metronome Sound"
option.label value="Beep" Bleep
option.label value="Click" Click
option.label value="Snare" Drum
br
select.metronome-select.metro-tempo title="Metronome Tempo"
- metronome_tempos.each do |t|
option.label value=t
=t
script#template-option type="text/template"
option value="{value}" title="{label}" selected="{selected}"
="{label}"
script#template-genre-option type="text/template"
option value="{value}"
="{label}"

View File

@ -59,6 +59,23 @@ script type="text/template" id='template-sync-viewer-recorded-track'
a.retry href='#'
= image_tag('content/icon_resync.png', width:12, height: 14)
script type="text/template" id='template-sync-viewer-recorded-backing-track'
.recorded-backing-track.sync data-id="{{data.id}}" data-recording-id="{{data.recording_id}}" data-client-id="{{data.client_id}}" data-client-track-id="{{data.client_track_id}}" data-track-id="{{data.backing_track_id}}" data-fully-uploaded="{{data.fully_uploaded}}"
.type
span.text BACKING
a.avatar-tiny href="#" user-id="{{data.user.id}}" hoveraction="musician"
img src="{{JK.resolveAvatarUrl(data.user.photo_url)}}"
.client-state.bar
.progress
span.msg
a.retry href='#'
= image_tag('content/icon_resync.png', width:12, height: 14)
.upload-state.bar
.progress
span.msg
a.retry href='#'
= image_tag('content/icon_resync.png', width:12, height: 14)
script type="text/template" id='template-sync-viewer-stream-mix'
.stream-mix.sync data-id="{{data.id}}" data-recording-id="{{data.recording_id}}"
@ -128,6 +145,33 @@ script type="text/template" id="template-sync-viewer-hover-recorded-track"
| {{data.summary}}
| {% } %}
script type="text/template" id="template-sync-viewer-hover-recorded-backing-track"
.help-hover-recorded-backing-tracks
.client-box
.client-state-info
span.special-text is the file on your system?
.client-state class="{{data.clientStateClass}}"
span.msg
| {{data.clientStateMsg}}
.client-state-definition.sync-definition
| {{data.clientStateDefinition}}
.upload-box
.upload-state-info
span.special-text is it uploaded?
.upload-state class="{{data.uploadStateClass}}"
span.msg
| {{data.uploadStateMsg}}
.upload-state-definition.sync-definition
| {{data.uploadStateDefinition}}
br clear="both"
| {% if(data.summary) { %}
.summary
.title what's next?
| {{data.summary}}
| {% } %}
script type="text/template" id="template-sync-viewer-hover-stream-mix"
.help-hover-stream-mix
@ -189,7 +233,7 @@ script type="text/template" id="template-sync-viewer-generic-command"
| {{data.displayType}}
script type="text/template" id="template-sync-viewer-recorded-track-command"
.recorded-track.sync
.recorded-track.track-item.sync
.type
.progress
span.text
@ -198,6 +242,15 @@ script type="text/template" id="template-sync-viewer-recorded-track-command"
img src="{{JK.resolveAvatarUrl(data.user.photo_url)}}"
img.instrument-icon data-instrument-id="{{data.instrument_id}}" hoveraction="instrument" src="{{JK.getInstrumentIconMap24()[data.instrument_id].asset}}"
script type="text/template" id="template-sync-viewer-recorded-backing-track-command"
.recorded-backing-track.track-item.sync
.type
.progress
span.text
| {{data.action}} BACKING TRACK
a.avatar-tiny href="#" user-id="{{data.user.id}}" hoveraction="musician"
img src="{{JK.resolveAvatarUrl(data.user.photo_url)}}"
script type="text/template" id="template-sync-viewer-log-item"
.log class="success-{{data.success}}"
.command

View File

@ -155,6 +155,9 @@
var openJamTrackDialog = new JK.OpenJamTrackDialog(JK.app);
openJamTrackDialog.initialize();
var openBackingTrackDialog = new JK.OpenBackingTrackDialog(JK.app);
openBackingTrackDialog.initialize();
var configureTracksDialog = new JK.ConfigureTracksDialog(JK.app);
configureTracksDialog.initialize();

View File

@ -33,3 +33,4 @@
= render 'dialogs/allSyncsDialog'
= render 'dialogs/adjustGearSpeedDialog'
= render 'dialogs/openJamTrackDialog'
= render 'dialogs/openBackingTrackDialog'

View File

@ -0,0 +1,44 @@
.dialog.openBackingTrackDialog-overlay.ftue-overlay.tall#open-backing-track-dialog layout="dialog" layout-id="open-backing-track-dialog"
.content-head
= image_tag "content/icon_add.png", {:width => 19, :height => 19, :class => 'content-icon' }
h1
| open an audio file
.dialog-inner
.recording-wrapper
table.open-backing-tracks cellspacing="0" cellpadding="0" border="0"
thead
tr
th align="left"
| NAME
th align="left"
| SIZE
th align="left"
| TYPE
tbody
br
/ .left.paginator-holder
.help-links
a.display-backingtracks-folder href='#'
| Display audio file folder
a.what-are-backingtracks href='#'
| What are Backing Tracks?
.right
a href="#" class="button-grey" layout-action="close"
| CANCEL
br clear="all"
script#template-backing-track-row type="text/template"
tr data-recording-id="{{data.backingTrackId}}" data-local-state="{{data.backingTrackState}}"
td
| {{data.name}}
td
| {{data.length}}
td
| {{data.type}}

View File

@ -39,7 +39,7 @@ if defined?(Bundler)
# Activate observers that should always be running.
# config.active_record.observers = :cacher, :garbage_collector, :forum_observer
config.active_record.observers = "JamRuby::InvitedUserObserver", "JamRuby::UserObserver", "JamRuby::FeedbackObserver", "JamRuby::RecordedTrackObserver", "JamRuby::QuickMixObserver"
config.active_record.observers = "JamRuby::InvitedUserObserver", "JamRuby::UserObserver", "JamRuby::FeedbackObserver", "JamRuby::RecordedTrackObserver", "JamRuby::QuickMixObserver", "JamRuby::RecordedBackingTrackObserver"
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
@ -312,5 +312,7 @@ if defined?(Bundler)
config.show_jamblaster_notice = true
config.show_jamblaster_kickstarter_link = true
config.metronome_available = true
config.backing_tracks_available = true
end
end

View File

@ -184,6 +184,10 @@ SampleApp::Application.routes.draw do
match '/sessions/:id/details/comments' => 'api_music_sessions#add_session_info_comment', :via => :post
match '/sessions/:id/jam_tracks/:jam_track_id/open' => 'api_music_sessions#jam_track_open', :via => :post
match '/sessions/:id/jam_tracks/close' => 'api_music_sessions#jam_track_close', :via => :post
match '/sessions/:id/backing_tracks/open' => 'api_music_sessions#backing_track_open', :via => :post
match '/sessions/:id/backing_tracks/close' => 'api_music_sessions#backing_track_close', :via => :post
match '/sessions/:id/metronome/open' => 'api_music_sessions#metronome_open', :via => :post
match '/sessions/:id/metronome/close' => 'api_music_sessions#metronome_close', :via => :post
# music session tracks
match '/sessions/:id/tracks' => 'api_music_sessions#track_create', :via => :post
@ -197,6 +201,9 @@ SampleApp::Application.routes.draw do
match '/music_notations' => 'api_music_notations#create', :via => :post
match '/music_notations/:id' => 'api_music_notations#download', :via => :get, :as => :download_music_notation
# Backing track_show
match '/backing_tracks' => 'api_backing_tracks#index', :via => :get, :as => 'api_backing_tracks_list'
# Jamtracks
match '/jamtracks' => 'api_jam_tracks#index', :via => :get, :as => 'api_jam_tracks_list'
match '/jamtracks/purchased' => 'api_jam_tracks#purchased', :via => :get, :as => 'api_jam_tracks_purchased'
@ -453,13 +460,22 @@ SampleApp::Application.routes.draw do
match '/recordings/:id/tracks/:track_id/upload_sign' => 'api_recordings#upload_sign', :via => :get
match '/recordings/:id/tracks/:track_id/upload_part_complete' => 'api_recordings#upload_part_complete', :via => :post
match '/recordings/:id/tracks/:track_id/upload_complete' => 'api_recordings#upload_complete', :via => :post
match '/recordings/:id/stream_mix/upload_next_part' => 'api_recordings#upload_next_part_stream_mix', :via => :get
# Recordings - stream_mix
match '/recordings/:id/stream_mix/upload_sign' => 'api_recordings#upload_sign_stream_mix', :via => :get
match '/recordings/:id/stream_mix/upload_part_complete' => 'api_recordings#upload_part_complete_stream_mix', :via => :post
match '/recordings/:id/stream_mix/upload_complete' => 'api_recordings#upload_complete_stream_mix', :via => :post
match '/recordings/:id/stream_mix/upload_next_part' => 'api_recordings#upload_next_part_stream_mix', :via => :get
# Recordings - backing tracks
match '/recordings/:id/backing_tracks/:track_id' => 'api_recordings#show_recorded_backing_track', :via => :get, :as => 'api_recordings_show_recorded_backing_track'
match '/recordings/:id/backing_tracks/:track_id/download' => 'api_recordings#backing_track_download', :via => :get, :as => 'api_recordings_download'
match '/recordings/:id/backing_tracks/:track_id/upload_next_part' => 'api_recordings#backing_track_upload_next_part', :via => :get
match '/recordings/:id/backing_tracks/:track_id/upload_sign' => 'api_recordings#backing_track_upload_sign', :via => :get
match '/recordings/:id/backing_tracks/:track_id/upload_part_complete' => 'api_recordings#backing_track_upload_part_complete', :via => :post
match '/recordings/:id/backing_tracks/:track_id/upload_complete' => 'api_recordings#backing_track_upload_complete', :via => :post
match '/recordings/:id/backing_tracks/:track_id/silent' => 'api_backing_tracks#backing_track_silent', :via => :post
# Recordings - recorded_videos
match '/recordings/:id/tracks/:video_id/upload_sign' => 'api_recordings#video_upload_sign', :via => :get
match '/recordings/:id/videos/:video_id/upload_start' => 'api_recordings#video_upload_start', :via => :post

View File

@ -140,10 +140,10 @@ class MusicSessionManager < BaseManager
Notification.send_session_depart(active_music_session, connection.client_id, user, recordingId)
end
def sync_tracks(active_music_session, client_id, new_tracks)
def sync_tracks(active_music_session, client_id, new_tracks, backing_tracks)
tracks = nil
active_music_session.with_lock do # VRFS-1297
tracks = Track.sync(client_id, new_tracks)
tracks = Track.sync(client_id, new_tracks, backing_tracks)
active_music_session.tick_track_changes
end
Notification.send_tracks_changed(active_music_session)

View File

@ -223,4 +223,87 @@ describe ApiMusicSessionsController do
json[:jam_track][:id].should == jam_track.id
end
end
describe "open_backing_track" do
let(:ams) { FactoryGirl.create(:active_music_session, creator: user) }
let(:backing_track) { "foo.mp3"}
it "does not allow someone to open a track unless they are in the session" do
post :backing_track_open, {:format => 'json', id: ams.id, backing_track_path: backing_track}
response.status.should == 403
end
it "does not allow someone to open a track unless they own the backing track" do
pending "connection with client to determine ownership"
conn.join_the_session(ams.music_session, true, tracks, user, 10)
post :backing_track_open, {:format => 'json', id: ams.id, backing_track_path: backing_track}
response.status.should == 403
end
it "allows someone who owns the backing track to open it" do
# put the connection of the user into the session, so th
conn.join_the_session(ams.music_session, true, tracks, user, 10)
post :backing_track_open, {:format => 'json', id: ams.id, backing_track_path: backing_track}
response.status.should == 200
end
it "does not allow someone to close a track unless they are in the session" do
post :backing_track_close, {:format => 'json', id: ams.id}
response.status.should == 403
end
it "allows the backing track to be closed" do
# put the connection of the user into the session, so th
conn.join_the_session(ams.music_session, true, tracks, user, 10)
post :backing_track_open, {:format => 'json', id: ams.id, backing_track_path: backing_track}
response.status.should == 200
post :backing_track_close, {:format => 'json', id: ams.id}
response.status.should == 200
end
end
describe "open_metronome" do
let(:ams) { FactoryGirl.create(:active_music_session, creator: user) }
let(:metronome) { "foo.mp3"}
it "does not allow someone to open a track unless they are in the session" do
post :metronome_open, {:format => 'json', id: ams.id, metronome_path: metronome}
response.status.should == 403
end
it "can open it" do
conn.join_the_session(ams.music_session, true, tracks, user, 10)
post :metronome_open, {:format => 'json', id: ams.id, metronome_path: metronome}
response.status.should == 200
end
it "does not allow someone to close a metronome" do
post :metronome_close, {:format => 'json', id: ams.id}
response.status.should == 403
end
it "does allow someone who joied to close a metronome" do
conn.join_the_session(ams.music_session, true, tracks, user, 10)
post :metronome_close, {:format => 'json', id: ams.id}
response.status.should == 200
end
it "allows the metronome to be closed" do
# put the connection of the user into the session, so th
conn.join_the_session(ams.music_session, true, tracks, user, 10)
post :metronome_open, {:format => 'json', id: ams.id, metronome_path: metronome}
response.status.should == 200
post :metronome_close, {:format => 'json', id: ams.id}
response.status.should == 200
end
end
end

View File

@ -10,6 +10,7 @@ describe ApiRecordingsController do
@music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true)
@connection = FactoryGirl.create(:connection, :user => @user, :music_session => @music_session)
@track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
@backing_track = FactoryGirl.create(:backing_track, :connection => @connection)
controller.current_user = @user
end
@ -100,7 +101,65 @@ describe ApiRecordingsController do
end
end
describe "download" do
describe "download track" do
let(:mix) { FactoryGirl.create(:mix) }
it "should only allow a user to download a track if they have claimed the recording" do
post :start, { :format => 'json', :music_session_id => @music_session.id }
response_body = JSON.parse(response.body)
recording = Recording.find(response_body['id'])
post :stop, { :format => 'json', :id => recording.id }
response.should be_success
end
it "is possible" do
mix.touch
recorded_track = mix.recording.recorded_tracks[0]
controller.current_user = mix.recording.owner
get :download, {id: recorded_track.recording.id, track_id: recorded_track.client_track_id}
response.status.should == 302
recorded_track.reload
recorded_track.download_count.should == 1
get :download, {id: recorded_track.recording.id, track_id: recorded_track.client_track_id}
response.status.should == 302
recorded_track.reload
recorded_track.download_count.should == 2
end
it "prevents download after limit is reached" do
mix.touch
recorded_track = mix.recording.recorded_tracks[0]
recorded_track.download_count = APP_CONFIG.max_audio_downloads
recorded_track.save!
controller.current_user = recorded_track.user
get :download, {format:'json', id: recorded_track.recording.id, track_id: recorded_track.client_track_id}
response.status.should == 404
JSON.parse(response.body, symbolize_names: true)[:message].should == "download limit surpassed"
end
it "lets admins surpass limit" do
mix.touch
recorded_track = mix.recording.recorded_tracks[0]
recorded_track.download_count = APP_CONFIG.max_audio_downloads
recorded_track.save!
recorded_track.user.admin = true
recorded_track.user.save!
controller.current_user = recorded_track.user
get :download, {format:'json', id: recorded_track.recording.id, track_id: recorded_track.client_track_id}
response.status.should == 302
recorded_track.reload
recorded_track.download_count.should == 101
end
end
describe "download backing track" do
let(:mix) { FactoryGirl.create(:mix) }
it "should only allow a user to download a track if they have claimed the recording" do

View File

@ -274,6 +274,11 @@ FactoryGirl.define do
sequence(:client_resource_id) { |n| "resource_id#{n}"}
end
factory :backing_track, :class => JamRuby::BackingTrack do
sequence(:client_track_id) { |n| "client_track_id#{n}"}
filename 'foo.mp3'
end
factory :video_source, :class => JamRuby::VideoSource do
#client_video_source_id "test_source_id"
sequence(:client_video_source_id) { |n| "client_video_source_id#{n}"}
@ -303,6 +308,19 @@ FactoryGirl.define do
association :recording, factory: :recording
end
factory :recorded_backing_track, :class => JamRuby::RecordedBackingTrack do
sequence(:client_id) { |n| "client_id-#{n}"}
sequence(:backing_track_id) { |n| "track_id-#{n}"}
sequence(:client_track_id) { |n| "client_track_id-#{n}"}
sequence(:filename) { |n| "filename-{#n}"}
sequence(:url) { |n| "/recordings/blah/#{n}"}
md5 'abc'
length 1
fully_uploaded true
association :user, factory: :user
association :recording, factory: :recording
end
factory :recorded_video, :class => JamRuby::RecordedVideo do
sequence(:recording_id) { |n| "recording_id-#{n}"}
sequence(:client_video_source_id) { |n| "client_video_source_id-#{n}"}

View File

@ -9,6 +9,10 @@ describe "Find Session", :js => true, :type => :feature, :capybara_feature => tr
let(:dallas) { dallas_geoip }
let(:user) { FactoryGirl.create(:user, last_jam_locidispid: austin_geoip[:locidispid], last_jam_addr: austin_ip) }
let(:finder) { FactoryGirl.create(:user, last_jam_locidispid: dallas_geoip[:locidispid], last_jam_addr: dallas_ip) }
before(:all) do
Capybara.default_wait_time = 20
end
before(:each) do
@ -82,7 +86,7 @@ describe "Find Session", :js => true, :type => :feature, :capybara_feature => tr
# this should cause it to move to the scheduled session view; we'll check all the values again
ActiveMusicSession.delete_all
sleep(3)
verify_find_session_score(nil, '#sessions-scheduled', session1_creator, session1_creator)
fast_signout

View File

@ -59,7 +59,8 @@ describe "In a Session", :js => true, :type => :feature, :capybara_feature => tr
end
leave_music_session_sleep_delay
in_client(finder) { expect(page).to_not have_selector('div.track-label', text: user.name) }
in_client(finder) { expect(page).to_not have_selector("div.track-label[title='#{user.name}']") }
end
many = 4
@ -110,6 +111,27 @@ describe "In a Session", :js => true, :type => :feature, :capybara_feature => tr
end
end
specify "metronome" do
pending "working through issue with fake media tracks"
user = FactoryGirl.create(:user)
invitee = FactoryGirl.create(:user)
FactoryGirl.create(:friendship, :user => user, :friend => invitee)
in_client(user) do
create_session
# Call it 10 times. The fake jam client will randomly ntp_stable:false 50% of the time
10.times do
find('#open-a-metronome').trigger(:click)
end
sleep(2)
# we should have received an error one of the times, but it should open eventually:
find('#notification').should have_text("Couldn't open metronome")
expect(page).to have_selector('.track-label[title="Metronome"]')
#save_screenshot("metro.png")
end
end
specify "invitee receives notification when creator invites musician" do
pending "blocked on testing this via front-end - fakeJamClient doesn't support invite UX"
user = FactoryGirl.create(:user)

View File

@ -192,6 +192,12 @@ bputs "before register capybara"
end
config.before(:all) do
# to reduce frequency of timeout on initial test
# https://github.com/teampoltergeist/poltergeist/issues/294#issuecomment-72746472
if self.respond_to? :visit
visit '/assets/application.css'
visit '/assets/application.js'
end
end
config.before(:each) do

View File

@ -637,7 +637,7 @@ def assert_all_tracks_seen(users=[])
users.each do |user|
in_client(user) do
users.reject {|u| u==user}.each do |other|
find('div.track-label', text: other.name)
find("div.track-label[title='#{other.name}']")
#puts user.name + " is able to see " + other.name + "\'s track"
end
end

View File

@ -709,6 +709,10 @@ module JamWebsockets
unless music_session_upon_reentry.nil? || music_session_upon_reentry.destroyed?
if music_session_upon_reentry.backing_track_initiator == user
music_session_upon_reentry.close_backing_track
end
# if a jamtrack is open and this user is no longer in the session, close it
if music_session_upon_reentry.jam_track_initiator == user
music_session_upon_reentry.close_jam_track