');
+ }
+
+ $inputDiv.find('input').val('');
}
function siteFailCallback($inputDiv) {
diff --git a/web/app/views/clients/_jam_track_preview.html.slim b/web/app/views/clients/_jam_track_preview.html.slim
new file mode 100644
index 000000000..f10de55c7
--- /dev/null
+++ b/web/app/views/clients/_jam_track_preview.html.slim
@@ -0,0 +1,10 @@
+script type="text/template" id='template-jam-track-preview'
+ .jam-track-preview
+ .actions
+ a.play-button href="#"
+ | Play
+ a.stop-button.hidden href="#"
+ | Stop
+ img.instrument-icon hoveraction="instrument" data-instrument-id="" width="24" height="24"
+ .instrument-name
+ .part
\ No newline at end of file
diff --git a/web/app/views/clients/_jamtrack_landing.html.slim b/web/app/views/clients/_jamtrack_landing.html.slim
index 853ee05fe..06e3efc2f 100644
--- a/web/app/views/clients/_jamtrack_landing.html.slim
+++ b/web/app/views/clients/_jamtrack_landing.html.slim
@@ -1,4 +1,4 @@
-#jamtrackLanding.screen.secondary layout='screen' layout-id='jamtrackLanding'
+#jamtrackLanding.screen.secondary.no-login-required layout='screen' layout-id='jamtrackLanding'
.content
.content-head
.content-icon=image_tag("content/icon_jamtracks.png", height:19, width:19)
diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb
index 2825ee519..49bcfa5f9 100644
--- a/web/app/views/clients/index.html.erb
+++ b/web/app/views/clients/index.html.erb
@@ -73,6 +73,7 @@
<%= render "listenBroadcast" %>
<%= render "sync_viewer_templates" %>
<%= render "download_jamtrack_templates" %>
+<%= render "jam_track_preview" %>
<%= render "help" %>
<%= render 'dialogs/dialogs' %>
diff --git a/web/app/views/landings/individual_jamtrack.html.slim b/web/app/views/landings/individual_jamtrack.html.slim
new file mode 100644
index 000000000..ad52b67f3
--- /dev/null
+++ b/web/app/views/landings/individual_jamtrack.html.slim
@@ -0,0 +1,48 @@
+- provide(:page_name, 'landing_page full landing_jamtrack individual_jamtrack')
+
+.two_by_two
+ .row
+ .column
+ h1.hidden.individualized
+ | Check Out Our
+ strong.jamtrack_name
+ | JamTrack
+ h1.hidden.generic
+ | We Have 100+ Amazing JamTracks, Check One Out!
+ p Click the play buttons below to hear the master mix and each fully isolated track. All are included in each single JamTrack.
+ .previews
+ .column
+ h1 See What You Can Do With JamTracks
+ .video-wrapper
+ .video-container
+ iframe src="//www.youtube.com/embed/gAJAIHMyois" frameborder="0" allowfullscreen
+ br clear="all"
+ .row
+ .column
+ h1
+ | Get Your First JamTrack Free Now!
+ p Click the GET A JAMTRACK FREE button below. Browse to find the one you want. Click Add to cart, and we'll apply a credit during checkout to make this first one free! We're confident you'll be back for more.
+ .browse-jamtracks-wrapper
+ a.white-bordered-button href="/client#/jamtrack" GET A JAMTRACK FREE!
+ .column
+ h1 Why Are JamTracks Different & Better?
+ p
+ | JamTracks are the best way to play with your favorite music.
+ | Unlike traditional backing tracks, JamTracks are complete multitrack recordings,
+ | with fully isolated tracks for each part. Used with the free JamKazam app/service, you can:
+ ul.jamtrack-reasons
+ li Solo just the individual track you want to play to hear and learn it
+ li Mute just the track you want to play, and play along with the rest
+ li Make audio recordings and share them via Facebook or URL
+ li Make video recordings and share them via YouTube or URL
+ li And even go online to play JamTracks with others in real time!
+ br clear="all"
+ br clear="all"
+
+javascript:
+
+
+ $(document).on('JAMKAZAM_READY', function(e, data) {
+ var song = new JK.IndividualJamTrack(data.app);
+ song.initialize();
+ })
diff --git a/web/app/views/landings/individual_jamtrack_band.html.slim b/web/app/views/landings/individual_jamtrack_band.html.slim
new file mode 100644
index 000000000..dad0f43c3
--- /dev/null
+++ b/web/app/views/landings/individual_jamtrack_band.html.slim
@@ -0,0 +1,46 @@
+- provide(:page_name, 'landing_page full landing_jamtrack individual_jamtrack_band')
+
+.two_by_two
+ .row
+ .column
+ h1
+ | We Have
+ span.jamtrack_band_info
+ |
+ span.jamtrack_noun JamTracks
+ span.check-it-out , Check One Out!
+ p Click the play buttons below to hear the master mix and each fully isolated track. All are included in each single JamTrack.
+ .previews
+ .column
+ h1 See What You Can Do With JamTracks
+ .video-wrapper
+ .video-container
+ iframe src="//www.youtube.com/embed/gAJAIHMyois" frameborder="0" allowfullscreen
+ br clear="all"
+ .row
+ .column
+ h1
+ | Get Your First JamTrack Free Now!
+ p Click the GET A JAMTRACK FREE button below. Browse to find the one you want. Click Add to cart, and we'll apply a credit during checkout to make this first one free! We're confident you'll be back for more.
+ .browse-jamtracks-wrapper
+ a.white-bordered-button href="/client#/jamtrack" GET A JAMTRACK FREE!
+ .column
+ h1 Why Are JamTracks Different & Better?
+ p
+ | JamTracks are the best way to play with your favorite music.
+ | Unlike traditional backing tracks, JamTracks are complete multitrack recordings,
+ | with fully isolated tracks for each part. Used with the free JamKazam app/service, you can:
+ ul.jamtrack-reasons
+ li Solo just the individual track you want to play to hear and learn it
+ li Mute just the track you want to play, and play along with the rest
+ li Make audio recordings and share them via Facebook or URL
+ li Make video recordings and share them via YouTube or URL
+ li And even go online to play JamTracks with others in real time!
+ br clear="all"
+ br clear="all"
+
+javascript:
+ $(document).on('JAMKAZAM_READY', function(e, data) {
+ var song = new JK.IndividualJamTrackBand(data.app);
+ song.initialize();
+ })
diff --git a/web/app/views/landings/product_jamblaster.html.slim b/web/app/views/landings/product_jamblaster.html.slim
new file mode 100644
index 000000000..c31e76678
--- /dev/null
+++ b/web/app/views/landings/product_jamblaster.html.slim
@@ -0,0 +1,37 @@
+- provide(:page_name, 'landing_page full landing_product product_jamblaster')
+
+.two_by_two
+ .row
+ .column
+ h1.product-headline
+ | The JamBlaster by JamKazam
+ p.product-description
+ | The JamBlaster is a device designed from the ground up to meet the unique requirements of real-time, online, distributed music performance. This device vastly extends the range/distance over which musicians can play together across the Internet.
+ ul
+ li Radically reduces audio processing latency compared to today's industry standard computers and audio interfaces.
+ li Delivers plug-and-play ease of use, with no worries about hardware and software incompatibilities, driver problems, and arcane configurations.
+ li Combines both a computer and an audio interface into a single elegant device.
+ li Works with computers (even old crappy ones), tablets or smartphones.
+ li Works with your favorite recording software applications like Garage Band, Reaper, Pro Tools, etc.
+ .column
+ h1 See What You Can Do With The JamBlaster
+ .video-wrapper
+ .video-container
+ iframe src="//www.youtube.com/embed/2Zk7-04IAx4" frameborder="0" allowfullscreen
+ br clear="all"
+ .row
+ .column
+ h1
+ | Want a JamBlaster? Need One?
+ p If you are a registered member of the JamKazam community, and if you "know" you will buy a JamBlaster for $199 as soon as they become available, then click the button below to add yourself to our wait list. When we get enough "virtual orders", we'll reach back out to all signups to take real orders.
+
+ .cta-big-button
+ a.white-bordered-button href="#" SIGN UP TO BUY A JAMBLASTER
+ .column
+ h1 Want To Know More About Latency?
+ p
+ | How is it possible that someone hundreds of miles away could feel like they are 20 feet away from you? Check out this video on latency to understand more:
+ .linked-video-holder
+ a href="https://www.youtube.com/watch?v=mB3_KMse-J4" rel="external" Watch Video About Latency - It's Pretty Fascinating
+ br clear="all"
+ br clear="all"
\ No newline at end of file
diff --git a/web/app/views/landings/product_jamtracks.html.slim b/web/app/views/landings/product_jamtracks.html.slim
new file mode 100644
index 000000000..a20b16584
--- /dev/null
+++ b/web/app/views/landings/product_jamtracks.html.slim
@@ -0,0 +1,42 @@
+- provide(:page_name, 'landing_page full landing_product product_jamtracks')
+
+.two_by_two
+ .row
+ .column
+ h1.product-headline
+ | JamTracks by JamKazam
+ p.product-description
+ | We have 100+ amazing JamTracks. Click the play buttons to hear the master mix and fully isolated track. All are included in each JamTrack.
+ .previews
+ .column
+ h1 See What You Can With JamTracks
+ .video-wrapper
+ .video-container
+ iframe src="//www.youtube.com/embed/2Zk7-04IAx4" frameborder="0" allowfullscreen
+ br clear="all"
+ .row
+ .column
+ h1
+ | Get Your First JamTrack Free Now!
+ p Click the GET A JAMTRACK FREE button below. Browse to find the one you want, click the Add to cart, and we'll apply a credit during checkout to make the first one free! We're confident you'll be back for more.
+
+ .cta-big-button
+ a.white-bordered-button href="/client#/jamtrack" GET A JAMTRACK FREE!
+ .column
+ h1 Why are JamTracks Better than Backing Tracks?
+ p
+ | JamTracks are the best way to play with your favorite music. Unlike traditional backing tracks, JamTracks are complete multitrack recordings, with fully isolated tracks for each part. Used with the free JamKazam app/service, you can:
+ ul.jamtrack-reasons
+ li Solo just the individual track you want to play to hear and learn it
+ li Mute just the track you want to play, and play along with the rest
+ li Make audio recordings and share them via Facebook or URL
+ li Make video recordings and share them via YouTube or URL
+ li And even go online to play JamTracks with others in real time!
+ br clear="all"
+ br clear="all"
+
+javascript:
+ $(document).on('JAMKAZAM_READY', function (e, data) {
+ var song = new JK.IndividualJamTrack(data.app);
+ song.initialize();
+ })
\ No newline at end of file
diff --git a/web/app/views/landings/product_platform.html.slim b/web/app/views/landings/product_platform.html.slim
new file mode 100644
index 000000000..16c2f351c
--- /dev/null
+++ b/web/app/views/landings/product_platform.html.slim
@@ -0,0 +1,40 @@
+- provide(:page_name, 'landing_page full landing_product product_platform')
+
+.two_by_two
+ .row
+ .column
+ h1.product-headline
+ | The JamKazam Platform
+ p.product-description
+ | JamKazam is an innovative live music platform and social network, enabling musicians to play music together in real time from different locations over the internet as if they are sitting in the same room. The core platform is free to use and delivers immense value:
+ ul
+ li Play music from home with your friends and bandmates without packing and transporting gear, and without needing a rehearsal space
+ li Connect with new musician friends from our community of thousands of musicians to play more often, explore new styles, learn from others
+ li Find musicians to join your band, or find a band to join, either virtual on JamKazam, or real world to meet and play in person
+ li Schedule sessions or jump into ad hoc jams
+ li Make and share recordings or session performances via Facebook or URL
+ li Live broadcast sessions to family, friends, and fans
+ li List your band for hire to play gigs at clubs and events
+ li List yourself for hire to play studio sessions or lay down recorded tracks remotely
+ .column
+ h1 See What You Can Do With JamKazam
+ .video-wrapper
+ .video-container
+ iframe src="//www.youtube.com/embed/ylYcvTY9CVo" frameborder="0" allowfullscreen
+ br clear="all"
+ .row
+ .column
+ h1
+ | Sign Up for JamKazam Now, It's Free!
+ p Yep, seriously. Sign up and start playing music online in real time with your friends - or make new ones from our community of thousands of musicians. It's free to play with others as much as you want.
+
+ .cta-big-button
+ a.white-bordered-button href="/signup" SIGN UP NOW FOR YOUR FREE ACCOUNT
+ .column
+ h1 Does This Really Work?
+ p
+ | Feeling skeptical about whether this can actually work? That's natural. We'd encourage you to watch a video of endorsements and kudos from just a few of the musicians who use JamKazam.
+ .linked-video-holder
+ a href="https://www.youtube.com/watch?v=_7qj5RXyHCo" rel="external" Check Out Endorsements Of Real Users
+ br clear="all"
+ br clear="all"
diff --git a/web/app/views/layouts/web.html.erb b/web/app/views/layouts/web.html.erb
index f3cd75c75..8c13c4a5d 100644
--- a/web/app/views/layouts/web.html.erb
+++ b/web/app/views/layouts/web.html.erb
@@ -85,6 +85,7 @@
<%= render "clients/help" %>
<%= render "clients/listenBroadcast" %>
<%= render "clients/flash" %>
+ <%= render "clients/jam_track_preview" %>
<%= render 'dialogs/dialogs' %>
diff --git a/web/app/views/spikes/jam_track_preview.html.slim b/web/app/views/spikes/jam_track_preview.html.slim
new file mode 100644
index 000000000..3ea46c7d3
--- /dev/null
+++ b/web/app/views/spikes/jam_track_preview.html.slim
@@ -0,0 +1,40 @@
+
+- provide(:title, 'Jam Track Preview')
+
+.content-wrapper
+ h2 Jam Track Preview
+
+ #players
+
+
+javascript:
+ var initialized = false;
+ $(document).on('JAMKAZAM_READY', function(e, data) {
+
+ var rest = JK.Rest();
+
+ if(gon.jamTrackPlanCode) {
+ rest.getJamTrack({plan_code: gon.jamTrackPlanCode})
+ .done(function(jamTrack) {
+ var $players = $('#players')
+
+ _.each(jamTrack.tracks, function(track) {
+
+ var $element = $('')
+
+ $players.append($element);
+
+ new JK.JamTrackPreview(data.app, $element, jamTrack, track, {master_shows_duration: true})
+ })
+ })
+ .fail(function() {
+ alert("couldn't fetch jam track")
+ })
+
+ }
+ else {
+ alert("You need to add ?jam_track_plan_code=jamtracks-acdc-backinblack for this to work (or any jamtrack 'plancode')")
+ }
+
+
+ })
diff --git a/web/config/application.rb b/web/config/application.rb
index aac520b7d..6ded2fb75 100644
--- a/web/config/application.rb
+++ b/web/config/application.rb
@@ -318,5 +318,7 @@ if defined?(Bundler)
config.metronome_available = true
config.backing_tracks_available = true
config.one_free_jamtrack_per_user = true
+
+ config.nominated_jam_track = 'jamtrack-pearljam-alive'
end
end
diff --git a/web/config/routes.rb b/web/config/routes.rb
index b10262fc3..393427b53 100644
--- a/web/config/routes.rb
+++ b/web/config/routes.rb
@@ -30,6 +30,13 @@ SampleApp::Application.routes.draw do
match '/landing/kick2', to: 'landings#watch_overview_kick2', via: :get, as: 'landing_kick2'
match '/landing/kick3', to: 'landings#watch_overview_kick3', via: :get, as: 'landing_kick3'
match '/landing/kick4', to: 'landings#watch_overview_kick4', via: :get, as: 'landing_kick4'
+ match '/landing/jamtracks/:plan_code', to: 'landings#individual_jamtrack', via: :get, as: 'individual_jamtrack'
+ match '/landing/jamtracks/band/:plan_code', to: 'landings#individual_jamtrack_band', via: :get, as: 'individual_jamtrack_band'
+
+ # product pages
+ match '/products/jamblaster', to: 'landings#product_jamblaster', via: :get, as: 'product_jamblaster'
+ match '/products/platform', to: 'landings#product_platform', via: :get, as: 'product_platform'
+ match '/products/jamtracks', to: 'landings#product_jamtracks', via: :get, as: 'product_jamtracks'
# oauth
match '/auth/:provider/callback', :to => 'sessions#oauth_callback'
@@ -94,7 +101,8 @@ SampleApp::Application.routes.draw do
match '/launch_app', to: 'spikes#launch_app'
match '/websocket', to: 'spikes#websocket'
match '/test_subscription', to: 'spikes#subscription'
- match '/widgets/download_jam_track', to: 'spikes#download_jam_track'
+ match '/widgets/download_jam_track', to: 'spikes #download_jam_track'
+ match '/widgets/jam_track_preview', to: 'spikes#jam_track_preview'
match '/site_validate', to: 'spikes#site_validate'
match '/recording_source', to: 'spikes#recording_source'
match '/musician_search_filter', to: 'spikes#musician_search_filter'
@@ -205,6 +213,8 @@ SampleApp::Application.routes.draw do
match '/backing_tracks' => 'api_backing_tracks#index', :via => :get, :as => 'api_backing_tracks_list'
# Jamtracks
+ match '/jamtracks/:plan_code' => 'api_jam_tracks#show', :via => :get, :as => 'api_jam_tracks_show'
+ match '/jamtracks/band/:plan_code' => 'api_jam_tracks#show_with_artist_info', :via => :get, :as => 'api_jam_tracks_show_with_artist_info'
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'
match '/jamtracks/download/:id' => 'api_jam_tracks#download', :via => :get, :as => 'api_jam_tracks_download'
diff --git a/web/lib/tasks/jam_tracks.rake b/web/lib/tasks/jam_tracks.rake
index 58b4dcf7b..dfdb0c9d8 100644
--- a/web/lib/tasks/jam_tracks.rake
+++ b/web/lib/tasks/jam_tracks.rake
@@ -24,4 +24,60 @@ namespace :jam_tracks do
JamTrackImporter.synchronize_all(skip_audio_upload:true)
end
+
+
+ task sync_master_preview_all: :environment do |task, args|
+ importer = JamTrackImporter.synchronize_jamtrack_master_previews
+ end
+
+ # syncs just one master track for a give JamTrack
+ task sync_master_preview: :environment do |task, args|
+ plan_code = ENV['PLAN_CODE']
+ if !plan_code
+ puts "PLAN_CODE must be set to something like jamtrack-acdc-backinblack"
+ exit(1)
+ end
+
+ jam_track = JamTrack.find_by_plan_code!(plan_code)
+
+ importer = JamTrackImporter.synchronize_jamtrack_master_preview(jam_track)
+
+ if importer.reason.nil? || importer.reason == "success" || importer.reason == "jam_track_exists"
+ puts("#{importer.name} #{importer.reason}")
+ else
+ puts("#{importer.name} failed to import.")
+ puts("#{importer.name} reason=#{importer.reason}")
+ puts("#{importer.name} detail=#{importer.detail}")
+ end
+ end
+
+
+ task sync_duration_all: :environment do |task, args|
+ importer = JamTrackImporter.synchronize_durations
+ end
+
+ # syncs just one master track for a give JamTrack
+ task sync_duration: :environment do |task, args|
+ plan_code = ENV['PLAN_CODE']
+ if !plan_code
+ puts "PLAN_CODE must be set to something like jamtrack-acdc-backinblack"
+ exit(1)
+ end
+
+ jam_track = JamTrack.find_by_plan_code!(plan_code)
+
+ importer = JamTrackImporter.synchronize_duration(jam_track)
+
+ if importer.reason.nil? || importer.reason == "success" || importer.reason == "jam_track_exists"
+ puts("#{importer.name} #{importer.reason}")
+ else
+ puts("#{importer.name} failed to import.")
+ puts("#{importer.name} reason=#{importer.reason}")
+ puts("#{importer.name} detail=#{importer.detail}")
+ end
+ end
+
+ task download_masters: :environment do |task, arg|
+ JamTrackImporter.download_masters
+ end
end
diff --git a/web/spec/features/individual_jamtrack_band_spec.rb b/web/spec/features/individual_jamtrack_band_spec.rb
new file mode 100644
index 000000000..5db9ac971
--- /dev/null
+++ b/web/spec/features/individual_jamtrack_band_spec.rb
@@ -0,0 +1,106 @@
+require 'spec_helper'
+
+describe "Individual JamTrack Band", :js => true, :type => :feature, :capybara_feature => true do
+
+ subject { page }
+
+ before(:all) do
+ ShoppingCart.delete_all
+ JamTrackRight.delete_all
+ JamTrack.delete_all
+ JamTrackTrack.delete_all
+ JamTrackLicensor.delete_all
+ end
+
+ let(:user) { FactoryGirl.create(:user) }
+ let(:jamtrack_acdc_backinblack) { @jamtrack_acdc_backinblack }
+
+ let(:billing_info) {
+ {
+ first_name: 'Seth',
+ last_name: 'Call',
+ address1: '10704 Buckthorn Drive',
+ city: 'Austin',
+ state: 'Texas',
+ country: 'US',
+ zip: '78759',
+ number: '4111111111111111',
+ month: '08',
+ year: '2017',
+ verification_value: '012'
+ }
+ }
+
+ def create_account(user, billing_info)
+ @recurlyClient.create_account(user, billing_info)
+ @created_accounts << user
+ end
+
+
+ before(:all) do
+
+ @recurlyClient = RecurlyClient.new
+ @created_accounts = []
+
+ @jamtrack_acdc_backinblack = FactoryGirl.create(:jam_track, name: 'Back in Black', original_artist: 'AC/DC', sales_region: 'United States', make_track: true, plan_code: 'jamtrack-acdc-backinblack')
+
+ # make sure plans are there
+ @recurlyClient.create_jam_track_plan(@jamtrack_acdc_backinblack) unless @recurlyClient.find_jam_track_plan(@jamtrack_acdc_backinblack)
+ end
+
+
+ after(:each) do
+ @created_accounts.each do |user|
+ if user.recurly_code
+ begin
+ @account = Recurly::Account.find(user.recurly_code)
+ if @account.present?
+ @account.destroy
+ end
+ rescue
+ end
+ end
+ end
+ end
+
+ describe "AC/DC Back in Black" do
+
+ it "logged out" do
+ visit "/landing/jamtracks/band/acdc-backinblack"
+
+ find('h1', text: "We Have 1 #{@jamtrack_acdc_backinblack.original_artist} JamTrack, Check It Out!")
+ jamtrack_acdc_backinblack.jam_track_tracks.each do |track|
+ if track.master?
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="other"]')
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:'Master Mix')
+ else
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="' + track.instrument.id + '"]')
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:track.instrument.description)
+ end
+ end
+ find('a.white-bordered-button')['href'].should eq("/client?artist=#{jamtrack_acdc_backinblack.original_artist}#/jamtrack")
+
+ find('a.white-bordered-button').trigger(:click)
+ find('h1', text: 'jamtracks')
+ end
+
+ it "logged in" do
+ fast_signin(user, "/landing/jamtracks/band/acdc-backinblack")
+
+ find('h1', text: "We Have 1 #{@jamtrack_acdc_backinblack.original_artist} JamTrack, Check It Out!")
+ jamtrack_acdc_backinblack.jam_track_tracks.each do |track|
+ if track.master?
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="other"]')
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:'Master Mix')
+ else
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="' + track.instrument.id + '"]')
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:track.instrument.description)
+ end
+ end
+ find('a.white-bordered-button')['href'].should eq("/client?artist=#{jamtrack_acdc_backinblack.original_artist}#/jamtrack")
+
+ find('a.white-bordered-button').trigger(:click)
+ find('h1', text: 'jamtracks')
+ end
+ end
+end
diff --git a/web/spec/features/individual_jamtrack_spec.rb b/web/spec/features/individual_jamtrack_spec.rb
new file mode 100644
index 000000000..7aa9db507
--- /dev/null
+++ b/web/spec/features/individual_jamtrack_spec.rb
@@ -0,0 +1,125 @@
+require 'spec_helper'
+
+describe "Individual JamTrack", :js => true, :type => :feature, :capybara_feature => true do
+
+ subject { page }
+
+ before(:all) do
+ ShoppingCart.delete_all
+ JamTrackRight.delete_all
+ JamTrack.delete_all
+ JamTrackTrack.delete_all
+ JamTrackLicensor.delete_all
+ end
+
+ let(:user) { FactoryGirl.create(:user) }
+ let(:jamtrack_acdc_backinblack) { @jamtrack_acdc_backinblack }
+
+ let(:billing_info) {
+ {
+ first_name: 'Seth',
+ last_name: 'Call',
+ address1: '10704 Buckthorn Drive',
+ city: 'Austin',
+ state: 'Texas',
+ country: 'US',
+ zip: '78759',
+ number: '4111111111111111',
+ month: '08',
+ year: '2017',
+ verification_value: '012'
+ }
+ }
+
+ def create_account(user, billing_info)
+ @recurlyClient.create_account(user, billing_info)
+ @created_accounts << user
+ end
+
+
+ before(:all) do
+
+ @recurlyClient = RecurlyClient.new
+ @created_accounts = []
+
+ @jamtrack_acdc_backinblack = FactoryGirl.create(:jam_track, name: 'Back in Black', original_artist: 'AC/DC', sales_region: 'United States', make_track: true, plan_code: 'jamtrack-acdc-backinblack')
+
+ # make sure plans are there
+ @recurlyClient.create_jam_track_plan(@jamtrack_acdc_backinblack) unless @recurlyClient.find_jam_track_plan(@jamtrack_acdc_backinblack)
+ end
+
+
+ after(:each) do
+ @created_accounts.each do |user|
+ if user.recurly_code
+ begin
+ @account = Recurly::Account.find(user.recurly_code)
+ if @account.present?
+ @account.destroy
+ end
+ rescue
+ end
+ end
+ end
+ end
+
+ describe "AC/DC Back in Black" do
+
+ it "logged out" do
+ visit "/landing/jamtracks/acdc-backinblack"
+
+ find('h1', text: 'Check Out Our Back in Black JamTrack')
+ jamtrack_acdc_backinblack.jam_track_tracks.each do |track|
+ if track.master?
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="other"]')
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:'Master Mix')
+ else
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="' + track.instrument.id + '"]')
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:track.instrument.description)
+ end
+ end
+ find('a.white-bordered-button')['href'].should eq("/client?artist=#{jamtrack_acdc_backinblack.original_artist}#/jamtrack")
+
+ find('a.white-bordered-button').trigger(:click)
+ find('h1', text: 'jamtracks')
+ end
+
+ it "logged in" do
+ fast_signin(user, "/landing/jamtracks/acdc-backinblack")
+
+ find('h1', text: 'Check Out Our Back in Black JamTrack')
+ jamtrack_acdc_backinblack.jam_track_tracks.each do |track|
+ if track.master?
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="other"]')
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:'Master Mix')
+ else
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="' + track.instrument.id + '"]')
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:track.instrument.description)
+ end
+ end
+ find('a.white-bordered-button')['href'].should eq("/client?artist=#{jamtrack_acdc_backinblack.original_artist}#/jamtrack")
+
+ find('a.white-bordered-button').trigger(:click)
+ find('h1', text: 'jamtracks')
+ end
+
+ it "generic version" do
+ visit "/landing/jamtracks/acdc-backinblack?generic=true"
+
+ find('h1', text: 'We Have 100+ Amazing JamTracks, Check One Out!')
+ jamtrack_acdc_backinblack.jam_track_tracks.each do |track|
+ if track.master?
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="other"]')
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:'Master Mix')
+ else
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] img.instrument-icon[data-instrument-id="' + track.instrument.id + '"]')
+ find('.jam-track-preview-holder[data-id="' + track.id + '"] .instrument-name', text:track.instrument.description)
+ end
+ end
+ find('a.white-bordered-button')['href'].should eq("/client#/jamtrack")
+
+ find('a.white-bordered-button').trigger(:click)
+ find('h1', text: 'jamtracks')
+ end
+ end
+end
diff --git a/web/spec/features/products_spec.rb b/web/spec/features/products_spec.rb
new file mode 100644
index 000000000..634d768f3
--- /dev/null
+++ b/web/spec/features/products_spec.rb
@@ -0,0 +1,122 @@
+require 'spec_helper'
+
+describe "Product Pages", :js => true, :type => :feature, :capybara_feature => true do
+
+ subject { page }
+
+ before(:all) do
+ ShoppingCart.delete_all
+ JamTrackRight.delete_all
+ JamTrack.delete_all
+ JamTrackTrack.delete_all
+ JamTrackLicensor.delete_all
+ end
+
+ let(:user) { FactoryGirl.create(:user) }
+ let(:jamtrack_acdc_backinblack) { @jamtrack_acdc_backinblack }
+
+ let(:billing_info) {
+ {
+ first_name: 'Seth',
+ last_name: 'Call',
+ address1: '10704 Buckthorn Drive',
+ city: 'Austin',
+ state: 'Texas',
+ country: 'US',
+ zip: '78759',
+ number: '4111111111111111',
+ month: '08',
+ year: '2017',
+ verification_value: '012'
+ }
+ }
+
+ def create_account(user, billing_info)
+ @recurlyClient.create_account(user, billing_info)
+ @created_accounts << user
+ end
+
+
+ before(:all) do
+
+ @recurlyClient = RecurlyClient.new
+ @created_accounts = []
+
+ @jamtrack_acdc_backinblack = FactoryGirl.create(:jam_track, name: 'Back in Black', original_artist: 'AC/DC', sales_region: 'United States', make_track: true, plan_code: 'jamtrack-acdc-backinblack')
+
+ # make sure plans are there
+ @recurlyClient.create_jam_track_plan(@jamtrack_acdc_backinblack) unless @recurlyClient.find_jam_track_plan(@jamtrack_acdc_backinblack)
+ end
+
+
+ after(:each) do
+ @created_accounts.each do |user|
+ if user.recurly_code
+ begin
+ @account = Recurly::Account.find(user.recurly_code)
+ if @account.present?
+ @account.destroy
+ end
+ rescue
+ end
+ end
+ end
+ end
+
+ describe "JamBlaster" do
+ it "logged out" do
+ visit "/products/jamblaster"
+
+ find('h1', text: 'The JamBlaster by JamKazam')
+ find('a.white-bordered-button')['href'].should eq("#") # nowhere to go yet
+ end
+
+ it "logged in" do
+ fast_signin(user, "/products/jamblaster")
+
+ find('h1', text: 'The JamBlaster by JamKazam')
+ find('a.white-bordered-button')['href'].should eq("#") # nowhere to go yet
+ end
+ end
+
+ describe "Platform" do
+ it "logged out" do
+ visit "/products/platform"
+
+ find('h1', text: 'The JamKazam Platform')
+ find('a.white-bordered-button').trigger(:click)
+
+ find('h2', text: 'Create your free JamKazam account')
+ end
+
+ it "logged in" do
+ fast_signin(user, "/products/platform")
+
+ find('h1', text: 'The JamKazam Platform')
+ find('a.white-bordered-button').trigger(:click)
+
+ # clicking /signup just redirects you to the client
+ find('h2', text: 'create session')
+ end
+ end
+
+ describe "JamTracks" do
+ it "logged out" do
+ visit "/products/jamtracks"
+
+ find('h1', text: 'JamTracks by JamKazam')
+ find('a.white-bordered-button').trigger(:click)
+
+ find('h1', text: 'jamtracks')
+ end
+
+ it "logged in" do
+ fast_signin(user, "/products/jamtracks")
+
+ find('h1', text: 'JamTracks by JamKazam')
+ find('a.white-bordered-button').trigger(:click)
+
+ find('h1', text: 'jamtracks')
+ end
+ end
+end
diff --git a/web/vendor/assets/javascripts/howler.core.js b/web/vendor/assets/javascripts/howler.core.js
new file mode 100644
index 000000000..33b8bfbc3
--- /dev/null
+++ b/web/vendor/assets/javascripts/howler.core.js
@@ -0,0 +1,1651 @@
+/*!
+ * howler.js v2.0.0-beta
+ * howlerjs.com
+ *
+ * (c) 2013-2015, James Simpson of GoldFire Studios
+ * goldfirestudios.com
+ *
+ * MIT License
+ */
+
+(function() {
+
+ 'use strict';
+
+ // Setup our audio context.
+ var ctx = null;
+ var usingWebAudio = true;
+ var noAudio = false;
+ setupAudioContext();
+
+ // Create a master gain node.
+ if (usingWebAudio) {
+ var masterGain = (typeof ctx.createGain === 'undefined') ? ctx.createGainNode() : ctx.createGain();
+ masterGain.gain.value = 1;
+ masterGain.connect(ctx.destination);
+ }
+
+ /** Global Methods **/
+ /***************************************************************************/
+
+ /**
+ * Create the global controller. All contained methods and properties apply
+ * to all sounds that are currently playing or will be in the future.
+ */
+ var HowlerGlobal = function() {
+ this.init();
+ };
+ HowlerGlobal.prototype = {
+ /**
+ * Initialize the global Howler object.
+ * @return {Howler}
+ */
+ init: function() {
+ var self = this || Howler;
+
+ // Internal properties.
+ self._codecs = {};
+ self._howls = [];
+ self._muted = false;
+ self._volume = 1;
+
+ // Set to false to disable the auto iOS enabler.
+ self.iOSAutoEnable = true;
+
+ // No audio is available on this system if this is set to true.
+ self.noAudio = noAudio;
+
+ // This will be true if the Web Audio API is available.
+ self.usingWebAudio = usingWebAudio;
+
+ // Expose the AudioContext when using Web Audio.
+ self.ctx = ctx;
+
+ // Check for supported codecs.
+ if (!noAudio) {
+ self._setupCodecs();
+ }
+
+ return self;
+ },
+
+ /**
+ * Get/set the global volume for all sounds.
+ * @param {Float} vol Volume from 0.0 to 1.0.
+ * @return {Howler/Float} Returns self or current volume.
+ */
+ volume: function(vol) {
+ var self = this || Howler;
+ vol = parseFloat(vol);
+
+ if (typeof vol !== 'undefined' && vol >= 0 && vol <= 1) {
+ self._volume = vol;
+
+ // When using Web Audio, we just need to adjust the master gain.
+ if (usingWebAudio) {
+ masterGain.gain.value = vol;
+ }
+
+ // Loop through and change volume for all HTML5 audio nodes.
+ for (var i=0; i 0 ? sound._seek : self._sprite[sprite][0] / 1000;
+ var duration = ((self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000) - seek;
+
+ // Create a timer to fire at the end of playback or the start of a new loop.
+ var ended = function() {
+ // Should this sound loop?
+ var loop = !!(sound._loop || self._sprite[sprite][2]);
+
+ // Fire the ended event.
+ self._emit('end', sound._id);
+
+ // Restart the playback for HTML5 Audio loop.
+ if (!self._webAudio && loop) {
+ self.stop(sound._id).play(sound._id);
+ }
+
+ // Restart this timer if on a Web Audio loop.
+ if (self._webAudio && loop) {
+ self._emit('play', sound._id);
+ sound._seek = sound._start || 0;
+ sound._playStart = ctx.currentTime;
+ self._endTimers[sound._id] = setTimeout(ended, ((sound._stop - sound._start) * 1000) / Math.abs(self._rate));
+ }
+
+ // Mark the node as paused.
+ if (self._webAudio && !loop) {
+ sound._paused = true;
+ sound._ended = true;
+ sound._seek = sound._start || 0;
+ self._clearTimer(sound._id);
+
+ // Clean up the buffer source.
+ sound._node.bufferSource = null;
+ }
+
+ // When using a sprite, end the track.
+ if (!self._webAudio && !loop) {
+ self.stop(sound._id);
+ }
+ };
+ self._endTimers[sound._id] = setTimeout(ended, (duration * 1000) / Math.abs(self._rate));
+
+ // Update the parameters of the sound
+ sound._paused = false;
+ sound._ended = false;
+ sound._sprite = sprite;
+ sound._seek = seek;
+ sound._start = self._sprite[sprite][0] / 1000;
+ sound._stop = (self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000;
+ sound._loop = !!(sound._loop || self._sprite[sprite][2]);
+
+ // Begin the actual playback.
+ var node = sound._node;
+ if (self._webAudio) {
+ // Fire this when the sound is ready to play to begin Web Audio playback.
+ var playWebAudio = function() {
+ self._refreshBuffer(sound);
+
+ // Setup the playback params.
+ var vol = (sound._muted || self._muted) ? 0 : sound._volume * Howler.volume();
+ node.gain.setValueAtTime(vol, ctx.currentTime);
+ sound._playStart = ctx.currentTime;
+
+ // Play the sound using the supported method.
+ if (typeof node.bufferSource.start === 'undefined') {
+ node.bufferSource.noteGrainOn(0, seek, duration);
+ } else {
+ node.bufferSource.start(0, seek, duration);
+ }
+
+ // Start a new timer if none is present.
+ if (!self._endTimers[sound._id]) {
+ self._endTimers[sound._id] = setTimeout(ended, (duration * 1000) / Math.abs(self._rate));
+ }
+
+ if (!args[1]) {
+ setTimeout(function() {
+ self._emit('play', sound._id);
+ }, 0);
+ }
+ };
+
+ if (self._loaded) {
+ playWebAudio();
+ } else {
+ // Wait for the audio to load and then begin playback.
+ self.once('load', playWebAudio);
+
+ // Cancel the end timer.
+ self._clearTimer(sound._id);
+ }
+ } else {
+ // Fire this when the sound is ready to play to begin HTML5 Audio playback.
+ var playHtml5 = function() {
+ node.currentTime = seek;
+ node.muted = sound._muted || self._muted || Howler._muted || node.muted;
+ node.volume = sound._volume * Howler.volume();
+ node.playbackRate = self._rate;
+ setTimeout(function() {
+ node.play();
+ if (!args[1]) {
+ self._emit('play', sound._id);
+ }
+ }, 0);
+ };
+
+ // Play immediately if ready, or wait for the 'canplaythrough'e vent.
+ if (node.readyState === 4 || !node.readyState && navigator.isCocoonJS) {
+ playHtml5();
+ } else {
+ var listener = function() {
+ // Setup the new end timer.
+ self._endTimers[sound._id] = setTimeout(ended, (duration * 1000) / Math.abs(self._rate));
+
+ // Begin playback.
+ playHtml5();
+
+ // Clear this listener.
+ node.removeEventListener('canplaythrough', listener, false);
+ };
+ node.addEventListener('canplaythrough', listener, false);
+
+ // Cancel the end timer.
+ self._clearTimer(sound._id);
+ }
+ }
+
+ return sound._id;
+ },
+
+ /**
+ * Pause playback and save current position.
+ * @param {Number} id The sound ID (empty to pause all in group).
+ * @return {Howl}
+ */
+ pause: function(id) {
+ var self = this;
+
+ // Wait for the sound to begin playing before pausing it.
+ if (!self._loaded) {
+ self.once('play', function() {
+ self.pause(id);
+ });
+
+ return self;
+ }
+
+ // If no id is passed, get all ID's to be paused.
+ var ids = self._getSoundIds(id);
+
+ for (var i=0; i Returns the group's volume value.
+ * volume(id) -> Returns the sound id's current volume.
+ * volume(vol) -> Sets the volume of all sounds in this Howl group.
+ * volume(vol, id) -> Sets the volume of passed sound id.
+ * @return {Howl/Number} Returns self or current volume.
+ */
+ volume: function() {
+ var self = this;
+ var args = arguments;
+ var vol, id;
+
+ // Determine the values based on arguments.
+ if (args.length === 0) {
+ // Return the value of the groups' volume.
+ return self._volume;
+ } else if (args.length === 1) {
+ // First check if this is an ID, and if not, assume it is a new volume.
+ var ids = self._getSoundIds();
+ var index = ids.indexOf(args[0]);
+ if (index >= 0) {
+ id = parseInt(args[0], 10);
+ } else {
+ vol = parseFloat(args[0]);
+ }
+ } else if (args.length === 2) {
+ vol = parseFloat(args[0]);
+ id = parseInt(args[1], 10);
+ }
+
+ // Update the volume or return the current volume.
+ var sound;
+ if (typeof vol !== 'undefined' && vol >= 0 && vol <= 1) {
+ // Wait for the sound to begin playing before changing the volume.
+ if (!self._loaded) {
+ self.once('play', function() {
+ self.volume.apply(self, args);
+ });
+
+ return self;
+ }
+
+ // Set the group volume.
+ if (typeof id === 'undefined') {
+ self._volume = vol;
+ }
+
+ // Update one or all volumes.
+ id = self._getSoundIds(id);
+ for (var i=0; i 0 ? Math.ceil((end - ctx.currentTime) * 1000) : 0);
+ }.bind(self, ids[i], sound), len);
+ } else {
+ var diff = Math.abs(from - to);
+ var dir = from > to ? 'out' : 'in';
+ var steps = diff / 0.01;
+ var stepLen = len / steps;
+
+ (function() {
+ var vol = from;
+ var interval = setInterval(function(id) {
+ // Update the volume amount.
+ vol += (dir === 'in' ? 0.01 : -0.01);
+
+ // Make sure the volume is in the right bounds.
+ vol = Math.max(0, vol);
+ vol = Math.min(1, vol);
+
+ // Round to within 2 decimal points.
+ vol = Math.round(vol * 100) / 100;
+
+ // Change the volume.
+ self.volume(vol, id);
+
+ // When the fade is complete, stop it and fire event.
+ if (vol === to) {
+ clearInterval(interval);
+ self._emit('faded', id);
+ }
+ }.bind(self, ids[i]), stepLen);
+ })();
+ }
+ }
+ }
+
+ return self;
+ },
+
+ /**
+ * Get/set the loop parameter on a sound. This method can optionally take 0, 1 or 2 arguments.
+ * loop() -> Returns the group's loop value.
+ * loop(id) -> Returns the sound id's loop value.
+ * loop(loop) -> Sets the loop value for all sounds in this Howl group.
+ * loop(loop, id) -> Sets the loop value of passed sound id.
+ * @return {Howl/Boolean} Returns self or current loop value.
+ */
+ loop: function() {
+ var self = this;
+ var args = arguments;
+ var loop, id, sound;
+
+ // Determine the values for loop and id.
+ if (args.length === 0) {
+ // Return the grou's loop value.
+ return self._loop;
+ } else if (args.length === 1) {
+ if (typeof args[0] === 'boolean') {
+ loop = args[0];
+ self._loop = loop;
+ } else {
+ // Return this sound's loop value.
+ sound = self._soundById(parseInt(args[0], 10));
+ return sound ? sound._loop : false;
+ }
+ } else if (args.length === 2) {
+ loop = args[0];
+ id = parseInt(args[1], 10);
+ }
+
+ // If no id is passed, get all ID's to be looped.
+ var ids = self._getSoundIds(id);
+ for (var i=0; i Returns the first sound node's current seek position.
+ * seek(id) -> Returns the sound id's current seek position.
+ * seek(seek) -> Sets the seek position of the first sound node.
+ * seek(seek, id) -> Sets the seek position of passed sound id.
+ * @return {Howl/Number} Returns self or the current seek position.
+ */
+ seek: function() {
+ var self = this;
+ var args = arguments;
+ var seek, id;
+
+ // Determine the values based on arguments.
+ if (args.length === 0) {
+ // We will simply return the current position of the first node.
+ id = self._sounds[0]._id;
+ } else if (args.length === 1) {
+ // First check if this is an ID, and if not, assume it is a new seek position.
+ var ids = self._getSoundIds();
+ var index = ids.indexOf(args[0]);
+ if (index >= 0) {
+ id = parseInt(args[0], 10);
+ } else {
+ id = self._sounds[0]._id;
+ seek = parseFloat(args[0]);
+ }
+ } else if (args.length === 2) {
+ seek = parseFloat(args[0]);
+ id = parseInt(args[1], 10);
+ }
+
+ // If there is no ID, bail out.
+ if (typeof id === 'undefined') {
+ return self;
+ }
+
+ // Wait for the sound to load before seeking it.
+ if (!self._loaded) {
+ self.once('load', function() {
+ self.seek.apply(self, args);
+ });
+
+ return self;
+ }
+
+ // Get the sound.
+ var sound = self._soundById(id);
+
+ if (sound) {
+ if (seek >= 0) {
+ // Pause the sound and update position for restarting playback.
+ var playing = self.playing(id);
+ if (playing) {
+ self.pause(id, true);
+ }
+
+ // Move the position of the track and cancel timer.
+ sound._seek = seek;
+ self._clearTimer(id);
+
+ // Restart the playback if the sound was playing.
+ if (playing) {
+ self.play(id, true);
+ }
+ } else {
+ if (self._webAudio) {
+ return (sound._seek + self.playing(id) ? ctx.currentTime - sound._playStart : 0);
+ } else {
+ return sound._node.currentTime;
+ }
+ }
+ }
+
+ return self;
+ },
+
+ /**
+ * Check if a specific sound is currently playing or not.
+ * @param {Number} id The sound id to check. If none is passed, first sound is used.
+ * @return {Boolean} True if playing and false if not.
+ */
+ playing: function(id) {
+ var self = this;
+ var sound = self._soundById(id) || self._sounds[0];
+
+ return sound ? !sound._paused : false;
+ },
+
+ /**
+ * Get the duration of this sound.
+ * @return {Number} Audio duration.
+ */
+ duration: function() {
+ return this._duration;
+ },
+
+ /**
+ * Unload and destroy the current Howl object.
+ * This will immediately stop all sound instances attached to this group.
+ */
+ unload: function() {
+ var self = this;
+
+ // Stop playing any active sounds.
+ var sounds = self._sounds;
+ for (var i=0; i= 0) {
+ Howler._howls.splice(index, 1);
+ }
+ }
+
+ // Delete this sound from the cache.
+ if (cache) {
+ delete cache[self._src];
+ }
+
+ // Clear out `self`.
+ self = null;
+
+ return null;
+ },
+
+ /**
+ * Listen to a custom event.
+ * @param {String} event Event name.
+ * @param {Function} fn Listener to call.
+ * @param {Number} id (optional) Only listen to events for this sound.
+ * @return {Howl}
+ */
+ on: function(event, fn, id) {
+ var self = this;
+ var events = self['_on' + event];
+
+ if (typeof fn === 'function') {
+ events.push({id: id, fn: fn});
+ }
+
+ return self;
+ },
+
+ /**
+ * Remove a custom event.
+ * @param {String} event Event name.
+ * @param {Function} fn Listener to remove. Leave empty to remove all.
+ * @param {Number} id (optional) Only remove events for this sound.
+ * @return {Howl}
+ */
+ off: function(event, fn, id) {
+ var self = this;
+ var events = self['_on' + event];
+
+ if (fn) {
+ // Loop through event store and remove the passed function.
+ for (var i=0; i=0; i--) {
+ if (cnt <= limit) {
+ return;
+ }
+
+ if (self._sounds[i]._ended) {
+ // Disconnect the audio source when using Web Audio.
+ if (self._webAudio && self._sounds[i]._node) {
+ self._sounds[i]._node.disconnect(0);
+ }
+
+ // Remove sounds until we have the pool size.
+ self._sounds.splice(i, 1);
+ cnt--;
+ }
+ }
+ },
+
+ /**
+ * Get all ID's from the sounds pool.
+ * @param {Number} id Only return one ID if one is passed.
+ * @return {Array} Array of IDs.
+ */
+ _getSoundIds: function(id) {
+ var self = this;
+
+ if (typeof id === 'undefined') {
+ var ids = [];
+ for (var i=0; i> (-2 * bc & 6)) : 0
+ ) {
+ buffer = chars.indexOf(buffer);
+ }
+
+ return output;
+ };
+
+ // Decode the base64 data URI without XHR, since some browsers don't support it.
+ var data = atob(url.split(',')[1]);
+ var dataView = new Uint8Array(data.length);
+ for (var i=0; i