├── .eslintrc
├── .gitignore
├── .npmignore
├── LICENSE.md
├── Makefile
├── Procfile
├── README.md
├── examples
├── callback.html
├── index.html
├── oembed.html
├── recording.css
├── recording.html
├── recording.js
└── streaming.html
├── index.js
├── karma.conf.js
├── package.json
├── serve.js
├── src
├── api.js
├── callback.js
├── config.js
├── connect.js
├── deferred.js
├── dialog
│ ├── dialog.js
│ ├── popup.js
│ └── store.js
├── player-api.js
├── recorder
│ ├── audiocontext.js
│ ├── getusermedia.js
│ └── recorder.js
└── stream.js
├── test
├── api-test.js
├── initialize-test.js
├── recorder-test.js
└── stream-test.js
├── vendor
├── playback
│ ├── index.js
│ ├── playback.js
│ └── webpack.config.js
└── recorderjs
│ ├── recorder.js
│ └── recorder.worker.js
└── webpack.config.js
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "node": true,
5 | "es6": true
6 | },
7 | "rules": {
8 | "camelcase": 0,
9 | "no-multi-spaces": 0,
10 | "no-new": 2,
11 | "no-underscore-dangle": 0,
12 | "quotes": [2, "single", "avoid-escape"],
13 | "space-after-keywords": [2, "always"],
14 | "strict": [2, "never"],
15 | "no-unused-vars": [2, { "vars": "all", "args": "none" }]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | sdk.js
3 | node_modules
4 | build
5 | .tmp
6 | /vendor/node
7 | /*.worker.js
8 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .git
2 | examples
3 | node_modules
4 | src
5 | test
6 | vendor
7 | .tmp
8 | build
9 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | SoundCloud JavaScript SDK Copyright (c) 2015 SoundCloud, Ltd. (MIT License)
2 | Recorderjs Copyright (c) 2013 Matt Diamond (MIT License)
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining
5 | a copy of this software and associated documentation files (the
6 | "Software"), to deal in the Software without restriction, including
7 | without limitation the rights to use, copy, modify, merge, publish,
8 | distribute, sublicense, and/or sell copies of the Software, and to
9 | permit persons to whom the Software is furnished to do so, subject to
10 | the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | OS := $(shell uname)
2 |
3 | BUILD_DIR := $(PWD)/build
4 | DESTDIR := $(BUILD_DIR)/system/$(OS)
5 | DESTBIN := $(DESTDIR)/bin
6 | NM_BIN := $(PWD)/node_modules/.bin
7 |
8 | DEP := vendor
9 |
10 | NODE_VERSION := 6.11.1
11 | NODE := nodejs-$(NODE_VERSION)
12 | NPM_BIN := $(DESTBIN)/npm
13 |
14 | export PATH := $(DESTBIN):$(NM_BIN):$(PATH)
15 |
16 | .PHONY: setup build sc-vendor-libs test run publish dirs clean
17 |
18 | node_modules: $(NPM_BIN) package.json
19 | $(NPM_BIN) install
20 | @touch $@
21 |
22 | setup: $(NPM_BIN)
23 |
24 | build: node_modules
25 | $(NPM_BIN) run build
26 |
27 | test: build
28 | $(NPM_BIN) run test
29 |
30 | run: build
31 | $(NPM_BIN) run serve
32 |
33 | run-with-watcher: build
34 | $(NPM_BIN) run start
35 |
36 | publish: test
37 | IS_NPM=1 $(NPM_BIN) run build
38 | $(NPM_BIN) publish
39 |
40 | dirs:
41 | echo $(DESTDIR)
42 | echo $(DESTBIN)
43 | echo $(NPM_BIN)
44 |
45 | clean:
46 | rm -rf $(NODE_MODULES) $(BUILD_DIR)/* $(TMP) $(DEP)/node sdk.js vendor/playback/playback.js
47 |
48 | sc-vendor-libs: node_modules
49 | $(NPM_BIN) install \
50 | @sc/scaudio \
51 | @sc/scaudio-public-api-stream-url-retriever \
52 | @sc/maestro-core \
53 | @sc/maestro-loaders \
54 | @sc/scaudio-controller-html5-player \
55 | @sc/scaudio-controller-hls-mse-player
56 | $(NPM_BIN) run buildPlayback
57 |
58 | $(NPM_BIN): $(DESTDIR)/usr/lib/$(NODE)/bin/node
59 | @mkdir -p $(@D)
60 | ln -sf $(DESTDIR)/usr/lib/$(NODE)/bin/npm $@
61 | @touch $@
62 |
63 | $(DESTDIR)/usr/lib/$(NODE)/bin/node: $(DEP)/node/$(OS)/$(NODE_VERSION).tar.gz
64 | @mkdir -p $(@D)
65 | tar xz -C $(DESTDIR)/usr/lib/$(NODE) --strip-components 1 -f $<
66 | @touch $@
67 |
68 | $(DEP)/node/$(OS)/$(NODE_VERSION).tar.gz:
69 | http_proxy=$(PROXY) curl -q --create-dirs --fail --location https://nodejs.org/dist/v$(NODE_VERSION)/node-v$(NODE_VERSION)-linux-x64.tar.xz --output $@
70 | @touch $@
71 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: node serve.js
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ⚠️⚠️DEPRECATED - NO LONGER MAINTAINED⚠️⚠️
2 | This repository is no longer maintained by the SoundCloud team due to capacity constraints. We're instead focusing our efforts on improving the API & the developer platform. Please note, at the time of updating this, the repo is already not in sync with the latest API changes.
3 |
4 | We recommend the community to fork this repo in order to maintain the SDK. We'd be more than happy to make a reference on our developer that the developers can use different SDKs build by the community. In case you need to reach out to us, please head over to https://github.com/soundcloud/api/issues
5 |
6 | ---
7 |
8 | # SoundCloud JavaScript Next
9 |
10 | ## Setup
11 |
12 | - `make setup`
13 |
14 | This will install the right node version locally. Please be patient. :)
15 |
16 | ## Building source
17 |
18 | - `make build`
19 |
20 | By default, the SDK is built into `build/sdk/sdk-VERSION.js`. Take a look at `webpack.config.js` for details.
21 |
22 | ### Running with the watcher
23 |
24 | This will run webpack with a watcher. The sdk will be rebuilt when you save changes in `src`.
25 |
26 | In addition, webpack will start a development server on `http://localhost:8080/`. This serves the files in the `examples/` folder.
27 |
28 | - `make run-with-watcher`
29 |
30 | ### Running without the watcher (custom server)
31 |
32 | - `make run`
33 |
34 | ## Running tests
35 |
36 | - `make test`
37 |
38 | The test suite uses Karma to execute the tests in Chrome, Firefox, and Safari if available.
39 |
40 |
--------------------------------------------------------------------------------
/examples/callback.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Callback
6 |
7 |
8 |
9 | This page should close soon
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SDK playground
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/examples/oembed.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SDK playground - oEmbed
6 |
7 |
31 |
32 |
33 |
34 | This example demonstrates the usage of the SDK's oEmbed method.
35 |
36 |
37 | It will embed a widget for the given URL and log the HTML code.
38 |
39 |
42 |
43 |
44 |
45 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/examples/recording.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: sans-serif;
4 | color: #fff;
5 | }
6 |
7 | video {
8 | position: absolute;
9 | z-index: 0;
10 | background-size: 100% 100%;
11 | top: 0px;
12 | left: 0px;
13 | min-width: 100%;
14 | min-height: 100%;
15 | width: auto;
16 | height: auto;
17 | cursor: pointer;
18 | }
19 |
20 | #recording {
21 | display: inline-block;
22 | z-index: 100;
23 | position: absolute;
24 | color: #f50;
25 | background-color: transparent;
26 | right: 15px;
27 | top: 15px;
28 | opacity: 0;
29 | font-size: 20px;
30 | transition: .2s opacity linear;
31 | text-shadow: 1px 1px rgba(0,0,0,.2);
32 | }
33 |
34 | #recording.visible {
35 | opacity: .9;
36 | }
37 |
38 | canvas {
39 | z-index: -1;
40 | position: absolute;
41 | }
42 |
43 | #overlay {
44 | z-index: 1;
45 | min-width: 100%;
46 | min-height: 100%;
47 | width: auto;
48 | height: auto;
49 | background-color: rgba(0,0,0,.8);
50 | position: absolute;
51 | display: flex;
52 | flex-direction: column;
53 | align-items: center;
54 | justify-content: center;
55 | }
56 |
57 | .button {
58 | background-color: #f50;
59 | color: #fff;
60 | border: none;
61 | border-radius: 5px;
62 | padding: 10px;
63 | font-size: 16px;
64 | display: inline-block;
65 | cursor: pointer;
66 | }
67 |
68 | .button:active {
69 | transform: translateY(1px);
70 | }
71 |
72 | a {
73 | color: #f50;
74 | text-decoration: none;
75 | }
76 |
--------------------------------------------------------------------------------
/examples/recording.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SDK playground - Recording
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | recording
15 |
16 |
17 |
18 | This app demonstrates how to record and upload songs with the SoundCloud SDK.
19 |
20 |
21 | After connecting your account, it will record audio as long as you press down on the video.
22 |
23 |
24 | Once you release the video, it will take a snapshot and stop the recording.
25 |
26 |
27 | Then it uploads both to SoundCloud as private track :)
28 |
29 |
Connect
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/examples/recording.js:
--------------------------------------------------------------------------------
1 | SC.initialize({
2 | client_id: 'YOUR_CLIENT_ID',
3 | redirect_uri: 'http://localhost:8080'
4 | });
5 |
6 | // background video
7 | var getUserMedia = navigator.getUserMedia ||
8 | navigator.webkitGetUserMedia ||
9 | navigator.mozGetUserMedia;
10 | var URL = window.URL || window.webkitURL;
11 | var audioContext = new (AudioContext || webkitAudioContext || mozAudioCntext)();
12 | var video = document.getElementById('video');
13 | var recording = document.getElementById('recording');
14 | var height, userMediaStream, width;
15 |
16 | // record flow
17 | var recorder;
18 | var startRecording = function(){
19 | recording.classList.add('visible');
20 | if (recorder) {
21 | recorder.stop();
22 | }
23 | var streamSource = audioContext.createMediaStreamSource(userMediaStream);
24 | recorder = new SC.Recorder({source: streamSource});
25 | recorder.start();
26 | };
27 |
28 | var stopRecording = function(){
29 | recording.classList.remove('visible');
30 | if (recorder) {
31 | recorder.stop();
32 | takePicture();
33 | }
34 | };
35 |
36 | video.addEventListener('mousedown', startRecording);
37 | video.addEventListener('touchstart', startRecording);
38 | video.addEventListener('mouseup', stopRecording);
39 | video.addEventListener('touchend', stopRecording);
40 |
41 | // get the user's camera feed
42 | getUserMedia.call(navigator, {video: true, audio: true}, function(stream){
43 | userMediaStream = stream;
44 | // convert stream to video feed and play it
45 | video.src = URL.createObjectURL(stream);
46 | video.play();
47 | video.volume = 0;
48 | }, function(error){
49 | alert('There was a problem in getting the video and audio stream from your device. Did you block the access?');
50 | });
51 |
52 | // read the video height when it is playing
53 | video.addEventListener('canplay', function(){
54 | if (!width || !height) {
55 | width = canvas.width = video.videoWidth;
56 | height = canvas.height = video.videoHeight;
57 | }
58 | });
59 |
60 | // take picture and upload
61 | var takePicture = function(){
62 | var context = canvas.getContext('2d');
63 | context.drawImage(video, 0, 0, width, height);
64 | canvas.toBlob(upload);
65 | };
66 |
67 | var upload = function(image){
68 | var title = prompt('Add a title for your recording', 'A very creative title!');
69 | recorder.getWAV().then(function(wav){
70 | var upload = SC.upload({
71 | title: title,
72 | sharing: 'private',
73 | asset_data: wav,
74 | artwork_data: image
75 | });
76 | upload.then(showTrack);
77 | upload.request.addEventListener('progress', function(event){
78 | message.innerHTML = 'Uploading (' + (event.loaded / event.total) * 100 + '%)';
79 | });
80 | });
81 | overlay.style.zIndex = 0;
82 | message.innerHTML = 'Uploading...';
83 | };
84 |
85 | var showTrack = function(track){
86 | message.innerHTML = 'Your track has been successfully uploaded!
';
87 | message.innerHTML += 'Check it out on SoundCloud';
88 | };
89 |
90 | // connect flow
91 | var connectButton = document.getElementById('connect');
92 | connectButton.addEventListener('click', function(){
93 | // connect to SoundCloud and disable the button
94 | SC.connect().then(function(){
95 | message.innerHTML = '';
96 | overlay.style.zIndex = -2;
97 | });
98 | });
99 |
--------------------------------------------------------------------------------
/examples/streaming.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SDK playground - Streaming
6 |
7 |
35 |
36 |
37 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const api = require('./src/api');
2 | const callback = require('./src/callback');
3 | const config = require('./src/config');
4 | const connect = require('./src/connect');
5 | const Promise = require('es6-promise').Promise;
6 | const Recorder = require('./src/recorder/recorder');
7 | const stream = require('./src/stream');
8 |
9 | module.exports = global.SC = {
10 | initialize (options = {}) {
11 | // set tokens
12 | config.set('oauth_token', options.oauth_token);
13 | config.set('client_id', options.client_id);
14 | config.set('redirect_uri', options.redirect_uri);
15 | config.set('baseURL', options.baseURL);
16 | config.set('connectURL', options.connectURL);
17 | },
18 |
19 | /** API METHODS */
20 | get (path, params) {
21 | return api.request('GET', path, params);
22 | },
23 |
24 | post (path, params) {
25 | return api.request('POST', path, params);
26 | },
27 |
28 | put (path, params) {
29 | return api.request('PUT', path, params);
30 | },
31 |
32 | delete (path) {
33 | return api.request('DELETE', path);
34 | },
35 |
36 | upload (options) {
37 | return api.upload(options);
38 | },
39 |
40 | /** CONNECT METHODS */
41 | connect (options) {
42 | return connect(options);
43 | },
44 |
45 | isConnected () {
46 | return config.get('oauth_token') !== undefined;
47 | },
48 |
49 | /** OEMBED METHODS */
50 | oEmbed (url, options) {
51 | return api.oEmbed(url, options);
52 | },
53 |
54 | /** RESOLVE METHODS */
55 | resolve (url) {
56 | return api.resolve(url);
57 | },
58 |
59 | /** RECORDER */
60 | Recorder: Recorder,
61 |
62 | /** PROMISE **/
63 | Promise: Promise,
64 |
65 | stream (trackPath, secretToken) {
66 | return stream(trackPath, secretToken);
67 | },
68 |
69 | connectCallback () {
70 | callback.notifyDialog(this.location);
71 | }
72 | };
73 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | const VERSION = require('./package.json').version
2 |
3 | module.exports = function(config) {
4 | config.set({
5 | browsers: ['Chrome', 'Firefox', 'Safari'],
6 |
7 | frameworks: ['chai', 'mocha', 'sinon'],
8 |
9 | files: [
10 | 'build/sdk/sdk-' + VERSION + '.js',
11 | 'test/*.js'
12 | ]
13 | });
14 | };
15 |
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "soundcloud",
3 | "version": "3.3.2",
4 | "description": "",
5 | "main": "sdk.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/soundcloud/soundcloud-javascript.git"
9 | },
10 | "scripts": {
11 | "test": "webpack && karma start --single-run",
12 | "build": "webpack -p",
13 | "buildPlayback": "cd ./vendor/playback && webpack --config webpack.config.js",
14 | "start": "IS_NPM=1 webpack-dev-server",
15 | "serve": "node ./serve.js"
16 | },
17 | "author": "SoundCloud Ltd.",
18 | "license": "MIT",
19 | "devDependencies": {
20 | "babel-core": "^5.6.15",
21 | "babel-loader": "^5.3.0",
22 | "chai": "^3.2.0",
23 | "eslint": "^1.0.0-rc-1",
24 | "karma": "^0.13.3",
25 | "karma-chai": "^0.1.0",
26 | "karma-chrome-launcher": "^0.2.0",
27 | "karma-firefox-launcher": "^0.1.6",
28 | "karma-mocha": "^0.2.0",
29 | "karma-safari-launcher": "^0.1.1",
30 | "karma-sinon": "^1.0.4",
31 | "mocha": "^2.2.5",
32 | "node-libs-browser": "^0.5.2",
33 | "sinon": "^1.15.4",
34 | "webpack": "^1.10.1",
35 | "webpack-dev-server": "^1.10.1",
36 | "worker-loader": "^0.6.0"
37 | },
38 | "dependencies": {
39 | "backbone-events-standalone": "^0.2.7",
40 | "es6-promise": "^2.3.0",
41 | "form-urlencoded": "^0.1.4",
42 | "node-static": "^0.7.7",
43 | "query-string": "^2.3.0"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/serve.js:
--------------------------------------------------------------------------------
1 | var static = require('node-static');
2 |
3 | var file = new static.Server('./public');
4 |
5 | require('http').createServer(function (request, response) {
6 | request.addListener('end', function () {
7 | file.serve(request, response);
8 | }).resume();
9 | }).listen(8080);
10 |
--------------------------------------------------------------------------------
/src/api.js:
--------------------------------------------------------------------------------
1 | const config = require('./config');
2 | const form = require('form-urlencoded');
3 | const Promise = require('es6-promise').Promise;
4 |
5 | const sendRequest = (method, url, data, progress) => {
6 | let xhr;
7 | const requestPromise = new Promise((resolve) => {
8 | const isFormData = global.FormData && (data instanceof FormData);
9 | xhr = new XMLHttpRequest();
10 |
11 | if (xhr.upload) {
12 | xhr.upload.addEventListener('progress', progress);
13 | }
14 | xhr.open(method, url, true);
15 |
16 | if (!isFormData) {
17 | xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
18 | }
19 |
20 | xhr.onreadystatechange = () => {
21 | if (xhr.readyState === 4) {
22 | resolve({responseText: xhr.responseText, request: xhr});
23 | }
24 | };
25 |
26 | xhr.send(data);
27 | });
28 |
29 | requestPromise.request = xhr;
30 | return requestPromise;
31 | };
32 |
33 | /**
34 | * Parses the public API's response and constructs error messages
35 | * @param {String} responseText The API's raw response
36 | * @param {XMLHttpRequest} xhr The original XMLHttpRequest
37 | * @return {Object({json, error})} An object containing the response and a possible error
38 | */
39 | const parseResponse = ({responseText, request}) => {
40 | let error, json;
41 | try {
42 | json = JSON.parse(responseText);
43 | } catch (e) {
44 |
45 | }
46 |
47 | if (!json) {
48 | if (request) {
49 | error = { message: `HTTP Error: ${request.status}` };
50 | } else {
51 | error = { message: 'Unknown error' };
52 | }
53 | } else if (json.errors) {
54 | error = { message: '' };
55 | if (json.errors[0] && json.errors[0].error_message) {
56 | error = { message: json.errors[0].error_message };
57 | }
58 | }
59 |
60 | if (error) {
61 | error.status = request.status;
62 | }
63 |
64 | return { json, error };
65 | };
66 |
67 | /**
68 | * Executes the public API request
69 | * @param {String} method The HTTP method (GET, POST, PUT, DELETE)
70 | * @param {String} url The resource's url
71 | * @param {Object} data Data to send along with the request
72 | * @param {Function=} progress upload progress handler
73 | * @return {Promise}
74 | */
75 | const sendAndFollow = (method, url, data, progress) => {
76 | const requestPromise = sendRequest(method, url, data, progress);
77 | const followPromise = requestPromise.then(({responseText, request}) => {
78 | const response = parseResponse({responseText, request});
79 |
80 | if (response.json && response.json.status === '302 - Found') {
81 | return sendAndFollow('GET', response.json.location, null);
82 | } else {
83 | if (request.status !== 200 && response.error) {
84 | throw response.error;
85 | } else {
86 | return response.json;
87 | }
88 | }
89 | });
90 | followPromise.request = requestPromise.request;
91 | return followPromise;
92 | };
93 |
94 | const addParams = (params, additionalParams, isFormData) => {
95 | Object.keys(additionalParams).forEach((key) => {
96 | if (isFormData) {
97 | params.append(key, additionalParams[key]);
98 | } else {
99 | params[key] = additionalParams[key];
100 | }
101 | });
102 | };
103 |
104 | module.exports = {
105 | /**
106 | * Executes the public API request
107 | * @param {String} method HTTP method
108 | * @param {String} path The resource's path
109 | * @param {(Object|FormData)} params Parameters that will be sent
110 | * @param {Function=} progress optional upload progress handler
111 | * @return {Promise}
112 | */
113 | request (method, path, params = {}, progress = () => {}) {
114 | const oauthToken = config.get('oauth_token');
115 | const clientId = config.get('client_id');
116 | const additionalParams = {};
117 | const isFormData = global.FormData && (params instanceof FormData);
118 | let data, url;
119 |
120 | additionalParams.format = 'json';
121 |
122 | // set the oauth_token or, in case none has been issued yet, the client_id
123 | if (oauthToken) {
124 | additionalParams.oauth_token = oauthToken;
125 | } else {
126 | additionalParams.client_id = clientId;
127 | }
128 |
129 | // add the additional params to the received params
130 | addParams(params, additionalParams, isFormData);
131 |
132 | // in case of POST, PUT, DELETE -> prepare data
133 | if (method !== 'GET') {
134 | data = isFormData ? params : form.encode(params);
135 | params = { oauth_token: oauthToken };
136 | }
137 |
138 | // prepend `/` if not present
139 | path = path[0] !== '/' ? `/${path}` : path;
140 |
141 | // construct request url
142 | url = `${config.get('baseURL')}${path}?${form.encode(params)}`;
143 |
144 | return sendAndFollow(method, url, data, progress);
145 | },
146 |
147 | /**
148 | * Fetches oEmbed information for the provided URL.
149 | * Also embeds the response into an element if provided in options
150 | * @param {String} trackUrl
151 | * @param {Object} options
152 | * @return {Promise}
153 | */
154 | oEmbed (trackUrl, options = {}) {
155 | // save element
156 | const element = options.element;
157 | delete options.element;
158 |
159 | options.url = trackUrl;
160 |
161 | // construct URL
162 | const url = `https://soundcloud.com/oembed.json?${form.encode(options)}`;
163 |
164 | // send the request and embed response into element if provided
165 | return sendAndFollow('GET', url, null).then((oEmbed) => {
166 | if (element && oEmbed.html) {
167 | element.innerHTML = oEmbed.html;
168 | }
169 | return oEmbed;
170 | });
171 | },
172 |
173 | /**
174 | * Uploads a track to SoundCloud
175 | * @param {Object} options The track's properties
176 | * @param {String} title The track's title
177 | * @param {Blob} file The track's data
178 | * @param {Blob=} artwork_data The track's artwork
179 | * @param {Function=} progress Progress callback
180 | * @return {Promise}
181 | */
182 | upload (options = {}) {
183 | const file = options.asset_data || options.file;
184 | const canMakeRequest = config.get('oauth_token') && options.title && file;
185 |
186 | if (!canMakeRequest) {
187 | return new Promise((resolve, reject) => {
188 | reject({
189 | status: 0,
190 | error_message: 'oauth_token needs to be present and title and asset_data / file passed as parameters'
191 | });
192 | });
193 | }
194 |
195 | const properties = Object.keys(options);
196 | const formData = new FormData();
197 |
198 | // add all data to formdata
199 | properties.forEach((property) => {
200 | let value = options[property];
201 | // `file` is used as short hand for `asset_data`
202 | if (property === 'file') {
203 | property = 'asset_data';
204 | value = options['file'];
205 | }
206 |
207 | formData.append(`track[${property}]`, value);
208 | });
209 |
210 | return this.request('POST', '/tracks', formData, options.progress);
211 | },
212 |
213 | /**
214 | * Resolves a SoundCloud url to a JSON representation of its entity
215 | * @param {String} url The URL that should get resolved
216 | * @return {Promise}
217 | */
218 | resolve (url) {
219 | return this.request('GET', '/resolve', {
220 | url: url,
221 | /*
222 | * Tell the API not to serve a redirect. This is to get around
223 | * CORS issues on Safari 7+, which likes to send pre-flight requests
224 | * before following redirects, which has problems.
225 | */
226 | _status_code_map: { 302: 200 }
227 | });
228 | }
229 | };
230 |
--------------------------------------------------------------------------------
/src/callback.js:
--------------------------------------------------------------------------------
1 | const qs = require('query-string');
2 | const dialogStore = require('./dialog/store');
3 |
4 | module.exports = {
5 | /**
6 | * Finds a dialog and passes it the callback's options
7 | * @param {Object} options The callback's options
8 | */
9 | notifyDialog (location) {
10 | // in the original implementation, values are read from search and hash
11 | // maybe this is due to the fact, that it might change in the future
12 | // using both values here then as well
13 | const searchParams = qs.parse(location.search);
14 | const hashParams = qs.parse(location.hash);
15 | const options = {
16 | oauth_token: searchParams.access_token || hashParams.access_token,
17 | dialog_id: searchParams.state || hashParams.state,
18 | error: searchParams.error || hashParams.error,
19 | error_description: searchParams.error_description || hashParams.error_description
20 | };
21 |
22 | const dialog = dialogStore.get(options.dialog_id);
23 | if (dialog) {
24 | dialog.handleConnectResponse(options);
25 | }
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | oauth_token: undefined,
3 | baseURL: 'https://api.soundcloud.com',
4 | connectURL: '//connect.soundcloud.com',
5 | client_id: undefined,
6 | redirect_uri: undefined
7 | };
8 |
9 | module.exports = {
10 | get(key) {
11 | return config[key];
12 | },
13 |
14 | set(key, value) {
15 | if (value) {
16 | config[key] = value;
17 | }
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/src/connect.js:
--------------------------------------------------------------------------------
1 | const config = require('./config');
2 | const Dialog = require('./dialog/dialog');
3 | const Promise = require('es6-promise').Promise;
4 |
5 | /**
6 | * Sets the oauth_token to the value that was provided by the callback
7 | * @param {Object} options The callback's parameters
8 | * @return {Object} The callback's parameters
9 | */
10 | const setOauthToken = (options) => {
11 | config.set('oauth_token', options.oauth_token);
12 | return options;
13 | };
14 |
15 | module.exports = function (options = {}) {
16 | // resolve immediately when oauth_token is set
17 | const oauth_token = config.get('oauth_token');
18 | if (oauth_token) {
19 | return new Promise((resolve) => { resolve({oauth_token}); });
20 | }
21 | // set up the options for the dialog
22 | // make `client_id`, `redirect_uri` and `scope` overridable
23 | const dialogOptions = {
24 | client_id: options.client_id || config.get('client_id'),
25 | redirect_uri: options.redirect_uri || config.get('redirect_uri'),
26 | response_type: 'code_and_token',
27 | scope: options.scope || 'non-expiring',
28 | display: 'popup'
29 | };
30 |
31 | // `client_id` and `redirect_uri` have to be passed
32 | if (!dialogOptions.client_id || !dialogOptions.redirect_uri) {
33 | throw new Error('Options client_id and redirect_uri must be passed');
34 | }
35 |
36 | // set up and open the dialog
37 | // set access token when user is done
38 | let dialog = new Dialog(dialogOptions);
39 | return dialog.open().then(setOauthToken);
40 | };
41 |
--------------------------------------------------------------------------------
/src/deferred.js:
--------------------------------------------------------------------------------
1 | const Promise = require('es6-promise').Promise;
2 |
3 | module.exports = function(){
4 | let deferred = {};
5 |
6 | deferred.promise = new Promise(function(resolve, reject) {
7 | deferred.resolve = resolve;
8 | deferred.reject = reject;
9 | });
10 |
11 | return deferred;
12 | };
13 |
--------------------------------------------------------------------------------
/src/dialog/dialog.js:
--------------------------------------------------------------------------------
1 | const deferred = require('../deferred');
2 | const dialogStore = require('./store');
3 | const popup = require('./popup');
4 | const qs = require('query-string');
5 |
6 | const ID_PREFIX = 'SoundCloud_Dialog';
7 |
8 | /**
9 | * Generates an id for the connect dialog
10 | * @return {String} id
11 | */
12 | const generateId = () => {
13 | return [ID_PREFIX, Math.ceil(Math.random() * 1000000).toString(16)].join('_');
14 | };
15 |
16 | /**
17 | * Build the SoundCloud connect url
18 | * @param {Object} options The options that will be passed on to the connect screen
19 | * @return {String} The constructed URL
20 | */
21 | const createURL = (options) => {
22 | return `https://soundcloud.com/connect?${qs.stringify(options)}`;
23 | }
24 |
25 | class Dialog {
26 | constructor (options = {}) {
27 | this.id = generateId();
28 | this.options = options;
29 | // will be used to identify the correct popup window
30 | this.options.state = this.id;
31 | this.width = 420;
32 | this.height = 670;
33 |
34 | this.deferred = deferred();
35 | }
36 |
37 | /**
38 | * Opens the dialog and returns a promise that fulfills when the
39 | * user has successfully connected
40 | * @return {Promise}
41 | */
42 | open () {
43 | const url = createURL(this.options);
44 | this.popup = popup.open(url, this.width, this.height);
45 | dialogStore.set(this.id, this);
46 | return this.deferred.promise;
47 | }
48 |
49 | /**
50 | * Resolves or rejects the dialog's promise based on the provided response.
51 | * (Is initiated from the callback module)
52 | * @param {Object} options The callback's response
53 | */
54 | handleConnectResponse (options) {
55 | const hasError = options.error;
56 | // resolve or reject the dialog's promise, based on the callback's response
57 | if (hasError) {
58 | this.deferred.reject(options);
59 | } else {
60 | this.deferred.resolve(options);
61 | }
62 | // close the popup
63 | this.popup.close();
64 | }
65 | }
66 |
67 | module.exports = Dialog;
68 |
--------------------------------------------------------------------------------
/src/dialog/popup.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | /**
3 | * Opens a centered popup with the specified URL
4 | * @param {String} url
5 | * @param {Number} width
6 | * @param {Number} height
7 | * @return {Window} A reference to the popup
8 | */
9 | open (url, width, height) {
10 | const options = {};
11 | let stringOptions;
12 |
13 | options.location = 1;
14 | options.width = width;
15 | options.height = height;
16 | options.left = window.screenX + (window.outerWidth - width) / 2;
17 | options.top = window.screenY + (window.outerHeight - height) / 2;
18 | options.toolbar = 'no';
19 | options.scrollbars = 'yes';
20 |
21 | stringOptions = Object.keys(options).map((key) => {
22 | return `${key}=${options[key]}`;
23 | }).join(', ');
24 |
25 | return window.open(url, options.name, stringOptions);
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/src/dialog/store.js:
--------------------------------------------------------------------------------
1 | const dialogStore = {};
2 |
3 | module.exports = {
4 | get (dialogId) {
5 | return dialogStore[dialogId];
6 | },
7 |
8 | set (dialogId, dialog) {
9 | dialogStore[dialogId] = dialog;
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/player-api.js:
--------------------------------------------------------------------------------
1 | const BackboneEvents = require('backbone-events-standalone');
2 | const { errors: { PlayerFatalError }, State } = require('../vendor/playback/playback').MaestroCore;
3 | const { errors: { NoStreamsError, NotSupportedError } } = require('../vendor/playback/playback').SCAudio;
4 |
5 | const TIMEUPDATE_INTERVAL = 1000 / 60;
6 |
7 | module.exports = function(scaudioPlayer) {
8 | function getState() {
9 | switch (scaudioPlayer.getState()) {
10 | case State.PLAYING:
11 | return 'playing';
12 | case State.PAUSED:
13 | return scaudioPlayer.isEnded() ? 'ended' : 'paused';
14 | case State.DEAD:
15 | return scaudioPlayer.getFatalError() ? 'error' : 'dead';
16 | case State.LOADING:
17 | default:
18 | return 'loading';
19 | }
20 | }
21 |
22 | function handleEmittingTimeEvents() {
23 | let timerId = 0;
24 | let previousPosition = null;
25 | scaudioPlayer.onChange.subscribe(({ playing, seeking, dead }) => {
26 | if (dead) {
27 | window.clearTimeout(timerId);
28 | } else if (playing !== undefined || seeking !== undefined) {
29 | doEmit();
30 | }
31 | });
32 | function doEmit() {
33 | window.clearTimeout(timerId);
34 | if (scaudioPlayer.isPlaying() && !scaudioPlayer.isEnded()) {
35 | timerId = window.setTimeout(doEmit, TIMEUPDATE_INTERVAL);
36 | }
37 | const newPosition = scaudioPlayer.getPosition();
38 | if (newPosition !== previousPosition) {
39 | previousPosition = newPosition;
40 | playerApi.trigger('time', newPosition);
41 | }
42 | }
43 | }
44 | let hadFirstPlay = false;
45 | scaudioPlayer.onStateChange.subscribe(() => playerApi.trigger('state-change', getState()));
46 | scaudioPlayer.onPlay.subscribe(() => {
47 | playerApi.trigger(hadFirstPlay ? 'play-resume' : 'play-start');
48 | hadFirstPlay = true;
49 | });
50 |
51 | scaudioPlayer.onPlayIntent.subscribe(() => playerApi.trigger('play'));
52 | scaudioPlayer.onPlayRejection.subscribe((playRejection) => playerApi.trigger('play-rejection', playRejection));
53 | scaudioPlayer.onPauseIntent.subscribe(() => playerApi.trigger('pause'));
54 | scaudioPlayer.onSeek.subscribe(() => playerApi.trigger('seeked'));
55 | scaudioPlayer.onSeekRejection.subscribe((seekRejection) => playerApi.trigger('seek-rejection', seekRejection));
56 | scaudioPlayer.onLoadStart.subscribe(() => playerApi.trigger('buffering_start'));
57 | scaudioPlayer.onLoadEnd.subscribe(() => playerApi.trigger('buffering_end'));
58 | scaudioPlayer.onEnded.subscribe(() => playerApi.trigger('finish'));
59 | scaudioPlayer.onError.subscribe((error) => {
60 | if (error instanceof NoStreamsError) {
61 | playerApi.trigger('no_streams');
62 | } else if (error instanceof NotSupportedError) {
63 | playerApi.trigger('no_protocol');
64 | } else if (error instanceof PlayerFatalError) {
65 | playerApi.trigger('audio_error');
66 | }
67 | });
68 |
69 | const playerApi = {
70 | play: scaudioPlayer.play.bind(scaudioPlayer),
71 | pause: scaudioPlayer.pause.bind(scaudioPlayer),
72 | seek: scaudioPlayer.seek.bind(scaudioPlayer),
73 | getVolume: scaudioPlayer.getVolume.bind(scaudioPlayer),
74 | setVolume: scaudioPlayer.setVolume.bind(scaudioPlayer),
75 | currentTime: scaudioPlayer.getPosition.bind(scaudioPlayer),
76 | getDuration: scaudioPlayer.getDuration.bind(scaudioPlayer),
77 | isBuffering: scaudioPlayer.isLoading.bind(scaudioPlayer),
78 | isPlaying: scaudioPlayer.isPlaying.bind(scaudioPlayer),
79 | isActuallyPlaying: scaudioPlayer.isActuallyPlaying.bind(scaudioPlayer),
80 | isEnded: scaudioPlayer.isEnded.bind(scaudioPlayer),
81 | isDead: scaudioPlayer.isDead.bind(scaudioPlayer),
82 | kill: scaudioPlayer.kill.bind(scaudioPlayer),
83 | hasErrored: () => !!scaudioPlayer.getFatalError(),
84 | getState
85 | };
86 | BackboneEvents.mixin(playerApi);
87 | handleEmittingTimeEvents();
88 | return playerApi;
89 | }
--------------------------------------------------------------------------------
/src/recorder/audiocontext.js:
--------------------------------------------------------------------------------
1 | const AudioContext = global.AudioContext || global.webkitAudioContext;
2 | let context = null;
3 |
4 | module.exports = () => {
5 | return context ? context : (context = new AudioContext());
6 | };
7 |
--------------------------------------------------------------------------------
/src/recorder/getusermedia.js:
--------------------------------------------------------------------------------
1 | const getUserMedia = global.navigator.getUserMedia ||
2 | global.navigator.webkitGetUserMedia ||
3 | global.navigator.mozGetUserMedia;
4 |
5 | module.exports = (options, success, error) => {
6 | getUserMedia.call(global.navigator, options, success, error);
7 | };
8 |
--------------------------------------------------------------------------------
/src/recorder/recorder.js:
--------------------------------------------------------------------------------
1 | const audioContext = require('./audiocontext');
2 | const getUserMedia = require('./getusermedia');
3 | const Promise = require('es6-promise').Promise;
4 | const RecorderJS = require('../../vendor/recorderjs/recorder');
5 |
6 |
7 | /**
8 | * Sets up the source node by either returning the provided source
9 | * or by requesting access to the browser's microphone
10 | * @return {Promise.} The AudioNode that has been set up
11 | */
12 | const initSource = function() {
13 | const context = this.context;
14 |
15 | // if a source was passed, use it, otherwise, request it
16 | return new Promise((resolve, reject) => {
17 | if (this.source) {
18 | if (!(this.source instanceof AudioNode)) {
19 | reject(new Error('source needs to be an instance of AudioNode'));
20 | } else {
21 | resolve(this.source);
22 | }
23 | } else {
24 | getUserMedia({audio: true}, ((stream) => {
25 | this.stream = stream;
26 | this.source = context.createMediaStreamSource(stream);
27 | resolve(this.source);
28 | }).bind(this), reject);
29 | }
30 | });
31 | }
32 |
33 | /**
34 | * Uses the Web Audio API to record audio and to play it.
35 | * Also leverages the internal api module to upload recordings
36 | */
37 | class Recorder {
38 |
39 | /**
40 | * Initializes the Recorder
41 | * @param {Object=} options
42 | * @param {AudioContext} options.context The AudioContext to use for recording
43 | * @param {AudioNode} options.source An AudioNode that should be used for recording
44 | */
45 | constructor (options = {}) {
46 | this.context = options.context || audioContext();
47 | this._recorder = null;
48 | this.source = options.source;
49 | this.stream = null;
50 | }
51 |
52 | /**
53 | * Starts the recording from the browser's microphone or
54 | * form the `source` that was provided in the constructor.
55 | * @return {Promise.} The AudioNode that is used for recording
56 | */
57 | start () {
58 | return initSource.call(this).then((source) => {
59 | this._recorder = new RecorderJS(source);
60 | this._recorder.record();
61 | return source;
62 | });
63 | }
64 |
65 | /**
66 | * Stops the recording
67 | */
68 | stop () {
69 | // stop the recording
70 | if (this._recorder) {
71 | this._recorder.stop();
72 | }
73 |
74 | // stop the input media stream
75 | if (this.stream) {
76 | // stream.stop() has been deprecated
77 | // https://developers.google.com/web/updates/2015/07/mediastream-deprecations?hl=en
78 | if (this.stream.stop) {
79 | this.stream.stop();
80 | } else if (this.stream.getTracks) {
81 | const stream = this.stream.getTracks()[0];
82 | if (stream) stream.stop();
83 | }
84 | }
85 | }
86 |
87 | /**
88 | * Creates a buffer from the recording
89 | * @return {Promise.} The AudioBuffer
90 | */
91 | getBuffer () {
92 | return new Promise((resolve, reject) => {
93 | if (this._recorder) {
94 | this._recorder.getBuffer(((buffer) => {
95 | const sampleRate = this.context.sampleRate;
96 | const theBuffer = this.context.createBuffer(2, buffer[0].length, sampleRate);
97 | theBuffer.getChannelData(0).set(buffer[0]);
98 | theBuffer.getChannelData(1).set(buffer[1]);
99 | resolve(theBuffer);
100 | }).bind(this));
101 | } else {
102 | reject(new Error('Nothing has been recorded yet.'));
103 | }
104 | });
105 | }
106 |
107 | /**
108 | * Creates a WAV blob from the recording
109 | * @return {Promise.} The recording as a WAV Blob
110 | */
111 | getWAV () {
112 | return new Promise((resolve, reject) => {
113 | if (this._recorder) {
114 | this._recorder.exportWAV((blob) => {
115 | resolve(blob);
116 | });
117 | } else {
118 | reject(new Error('Nothing has been recorded yet.'));
119 | }
120 | });
121 | }
122 |
123 | /**
124 | * Plays the recording
125 | * @return {Promise.} The AudioNode that is used to play the recording
126 | */
127 | play () {
128 | return this.getBuffer().then((buffer) => {
129 | const bufferSource = this.context.createBufferSource();
130 | bufferSource.buffer = buffer;
131 | bufferSource.connect(this.context.destination);
132 | bufferSource.start(0);
133 | return bufferSource;
134 | });
135 | }
136 |
137 | /**
138 | * Initiates the download of the wav file
139 | * @param {[type]} filename [description]
140 | * @return {[type]} [description]
141 | */
142 | saveAs (filename) {
143 | return this.getWAV().then((blob) => {
144 | RecorderJS.forceDownload(blob, filename);
145 | });
146 | }
147 |
148 | /**
149 | * Deletes and stops the recording
150 | */
151 | delete () {
152 | if (this._recorder) {
153 | this._recorder.stop();
154 | this._recorder.clear();
155 | this._recorder = null;
156 | }
157 |
158 | if (this.stream) {
159 | this.stream.stop();
160 | }
161 | }
162 | }
163 |
164 | module.exports = Recorder;
165 |
--------------------------------------------------------------------------------
/src/stream.js:
--------------------------------------------------------------------------------
1 | const api = require('./api');
2 | const config = require('./config');
3 | const playerApi = require('./player-api');
4 | const SCAudio = require('../vendor/playback/playback').SCAudio;
5 | const maestroLogger = require('../vendor/playback/playback').MaestroCore.logger;
6 | const StreamUrlRetriever = require('../vendor/playback/playback').SCAudioPublicApiStreamURLRetriever.StreamUrlRetriever;
7 | const MediaElementManager = require('../vendor/playback/playback').SCAudioControllerHTML5Player.MediaElementManager;
8 | const HTML5PlayerController = require('../vendor/playback/playback').SCAudioControllerHTML5Player.HTML5PlayerController;
9 | const HLSMSEPlayerController = require('../vendor/playback/playback').SCAudioControllerHLSMSEPlayer.HLSMSEPlayerController;
10 | const stringLoader = require('../vendor/playback/playback').MaestroLoaders.stringLoader;
11 |
12 | const mediaElementManager = new MediaElementManager('audio', maestroLogger.noOpLogger);
13 |
14 | /**
15 | * Fetches track info and instantiates a player for the track
16 | * @param {String} trackPath The track's path (/tracks/:track_id)
17 | * @param {String=} secretToken If the track is secret, provide the secret token here
18 | * @return {Promise}
19 | */
20 | module.exports = (trackPath, secretToken) => {
21 | const options = secretToken ? {secret_token: secretToken} : {};
22 |
23 | return api.request('GET', trackPath, options).then((track) => {
24 | function registerPlay() {
25 | let registerEndpoint = `${baseURL}/tracks/${encodeURIComponent(track.id)}/plays?client_id=${encodeURIComponent(clientId)}`;
26 | if (secretToken) {
27 | registerEndpoint += `&secret_token=${encodeURIComponent(secretToken)}`;
28 | }
29 | const xhr = new XMLHttpRequest();
30 | xhr.open('POST', registerEndpoint, true);
31 | xhr.send();
32 | }
33 |
34 | const baseURL = config.get('baseURL')
35 | const clientId = config.get('client_id');
36 | const oauthToken = config.get('oauth_token');
37 |
38 | let playRegistered = false;
39 | const streamUrlRetriever = new StreamUrlRetriever({
40 | clientId,
41 | secretToken,
42 | trackId: track.id,
43 | requestAuthorization: oauthToken ? 'OAuth ' + oauthToken : null,
44 | loader: stringLoader
45 | });
46 |
47 | const player = new SCAudio.Player({
48 | controllers: [
49 | new HLSMSEPlayerController(mediaElementManager),
50 | new HTML5PlayerController(mediaElementManager)
51 | ],
52 | streamUrlRetriever,
53 | getURLOpts: { preview: track.policy === 'SNIP' },
54 | streamUrlsExpire: true,
55 | mediaSessionEnabled: true,
56 | logger: maestroLogger.noOpLogger
57 | });
58 |
59 | player.onPlay.subscribe(() => {
60 | if (!playRegistered) {
61 | playRegistered = true;
62 | registerPlay();
63 | }
64 | });
65 | player.onEnded.subscribe(() => {
66 | // maestro keeps the old playing state when at the end. Call pause() to maintain backwards compatibility
67 | player.pause();
68 | });
69 | player.onPlayIntent.subscribe(() => {
70 | if (player.isEnded()) {
71 | // seek back to 0 if the user calls play() and we're at the end.
72 | player.seek(0);
73 | }
74 | });
75 | return playerApi(player);
76 | });
77 | };
78 |
79 | /**
80 | * Call this from a user interaction, before creating a player, to ensure that playback
81 | * can start even if `play()` is not from a user interaction.
82 | */
83 | module.exports.activateAudioElement = () => {
84 | mediaElementManager.activate();
85 | };
86 |
--------------------------------------------------------------------------------
/test/api-test.js:
--------------------------------------------------------------------------------
1 | describe('API methods', function () {
2 | beforeEach(function () {
3 | this.xhr = sinon.useFakeXMLHttpRequest();
4 |
5 | this.requests = [];
6 | this.xhr.onCreate = function(xhr) {
7 | this.requests.push(xhr);
8 | }.bind(this);
9 |
10 | SC.initialize({
11 | client_id: 'YOUR_CLIENT_ID',
12 | redirect_uri: 'http://localhost:8080'
13 | });
14 | });
15 |
16 | afterEach(function() {
17 | this.xhr.restore();
18 | });
19 |
20 | it('should return the request with the promise', function(){
21 | var promise = SC.get('tracks/its-a-deep-bark');
22 | assert.isNotNull(promise.request);
23 | assert.instanceOf(promise.request, XMLHttpRequest);
24 | });
25 |
26 | it('should return the correct status if a request has an empty response', function(done){
27 | SC.get('tracks/its-a-deep-bark342').then(function(){
28 | assert.fail();
29 | done()
30 | }).catch(function(error){
31 | assert.equal(error.status, 404);
32 | assert.equal(error.message, 'HTTP Error: 404');
33 | done();
34 | });
35 | this.requests[0].respond(404,
36 | { 'Content-Type': 'text/json' },
37 | '');
38 | });
39 |
40 | describe('GET calls', function () {
41 | it('should make a GET against tracks', function (done) {
42 | SC.get('tracks/its-a-deep-bark').then(function (track) {
43 | assert.equal(track.kind, 'track', 'track GET returns a track');
44 | assert.equal(this.requests[0].method, 'GET');
45 | done();
46 | }.bind(this)).catch(function (err) {
47 | done(err);
48 | });
49 | this.requests[0].respond(200,
50 | { 'Content-Type': 'text/json' },
51 | '{"kind": "track"}');
52 | });
53 |
54 | it('should trigger a promise catch on a 500 with bad JSON', function (done) {
55 | SC.get('tracks/its-a-deep-bark').then(function (track) {
56 | assert.fail('Promise should not resolve');
57 | done();
58 | }.bind(this)).catch(function (err) {
59 | assert.ok(err, 'Promise.catch is sent an error object');
60 | assert.equal(err.status, 500, 'error.status is a 500');
61 | done();
62 | });
63 | this.requests[0].respond(500,
64 | { 'Content-Type': 'text/json' },
65 | '{"errors": [{"error_message": "server explosion"}]');
66 | });
67 |
68 | it('should trigger a promise catch on a 500 with error JSON', function (done) {
69 | SC.get('tracks/its-a-deep-bark').then(function (track) {
70 | assert.fail('Promise should not resolve');
71 | done();
72 | }.bind(this)).catch(function (err) {
73 | assert.ok(err, 'Promise.catch is sent and error object');
74 | assert.equal(err.status, 500, 'error.status is a 500');
75 | assert.equal(err.message, 'server explosion');
76 | done();
77 | });
78 | this.requests[0].respond(500,
79 | { 'Content-Type': 'text/json' },
80 | '{"errors": [{"error_message": "server explosion"}]}');
81 | });
82 |
83 | it('should trigger a promise catch on a 404', function (done) {
84 | SC.get('tracks/its-a-deep-bark').then(function (track) {
85 | assert.fail('Promise should not resolve');
86 | done();
87 | }.bind(this)).catch(function (err) {
88 | assert.ok(err, 'Promise.catch is sent and error object');
89 | assert.equal(err.status, 404, 'error.status is a 404');
90 | assert.equal(err.message, 'not found');
91 | done();
92 | });
93 | this.requests[0].respond(404,
94 | { 'Content-Type': 'text/json' },
95 | '{"errors": [{"error_message": "not found"}]}');
96 | });
97 |
98 | it('should use the oauth_token if it is set', function (done) {
99 | SC.initialize({
100 | client_id: 'YOUR_CLIENT_ID',
101 | redirect_uri: 'http://localhost:8080',
102 | oauth_token: 'SOME_OAUTH_TOKEN'
103 | });
104 |
105 | SC.get('tracks/its-a-deep-bark').then(function (track) {
106 | done();
107 | }.bind(this)).catch(function (err) {
108 | done();
109 | });
110 |
111 | assert.match(this.requests[0].url, /oauth_token=SOME_OAUTH_TOKEN/, 'ouath token exists in URL');
112 | assert.notMatch(this.requests[0].url, /client_id=YOUR_CLIENT_ID/, 'client id does not exist in URL');
113 | this.requests[0].respond(404,
114 | { 'Content-Type': 'text/json' },
115 | '{"errors": [{"error_message": "not found"}]}');
116 | });
117 |
118 | });
119 |
120 | it('should add the oauth_token to the query for non GET requests', function (done) {
121 | SC.initialize({
122 | client_id: 'YOUR_CLIENT_ID',
123 | redirect_uri: 'http://localhost:8080',
124 | oauth_token: 'SOME_OAUTH_TOKEN'
125 | });
126 |
127 | SC.put('tracks').then(function (track) {
128 | done();
129 | });
130 |
131 | assert.match(this.requests[0].url, /oauth_token=SOME_OAUTH_TOKEN/, 'ouath token exists in URL');
132 | assert.notMatch(this.requests[0].url, /client_id=YOUR_CLIENT_ID/, 'client id does not exist in URL');
133 | this.requests[0].respond(200, { 'Content-Type': 'text/json' }, '{}');
134 | });
135 |
136 | it('should make a POST when post is called', function (done) {
137 | SC.post('posting', {foo: 'bar'}).then(function (res) {
138 | assert.equal(this.requests[0].method, 'POST');
139 | done();
140 | }.bind(this)).catch(function (err) {
141 | done(err);
142 | });
143 | this.requests[0].respond(200,
144 | { 'Content-Type': 'text/json' },
145 | '{"kind": "track"}');
146 | });
147 |
148 |
149 | it('should make a PUT when put is called', function (done) {
150 | SC.put('putting', {foo: 'bar'}).then(function (res) {
151 | assert.equal(this.requests[0].method, 'PUT');
152 | done();
153 | }.bind(this)).catch(function (err) {
154 | done(err);
155 | });
156 | this.requests[0].respond(200,
157 | { 'Content-Type': 'text/json' },
158 | '{"kind": "track"}');
159 | });
160 |
161 | it('should make a DELETE when delete is called', function (done) {
162 | SC['delete']('deleting', {foo: 'bar'}).then(function (res) {
163 | assert.equal(this.requests[0].method, 'DELETE');
164 | done();
165 | }.bind(this)).catch(function (err) {
166 | done(err);
167 | });
168 | this.requests[0].respond(200,
169 | { 'Content-Type': 'text/json' },
170 | '{"status": "200 - OK"}');
171 | });
172 |
173 | describe('UPLOAD', function(){
174 | if (!window.FormData) {
175 | console.log('Browser does not support FormData, will skip upload tests.');
176 | return;
177 | }
178 |
179 | it('should send FormData for uploads', function(){
180 | SC.initialize({
181 | client_id: 'YOUR_CLIENT_ID',
182 | redirect_uri: 'http://localhost:8080',
183 | oauth_token: 'THE_TOKEN'
184 | });
185 |
186 | SC.upload({asset_data: {}, title: {}});
187 | assert.ok(this.requests[0].requestBody instanceof FormData);
188 | });
189 |
190 | it('should allow `file` instead of asset_data', function(){
191 | SC.initialize({
192 | client_id: 'YOUR_CLIENT_ID',
193 | redirect_uri: 'http://localhost:8080',
194 | oauth_token: 'THE_TOKEN'
195 | });
196 |
197 | SC.upload({file: {}, title: {}});
198 | assert.ok(this.requests[0].requestBody instanceof FormData);
199 | });
200 |
201 | it('should fail if not all needed params specified', function(done){
202 | SC.upload({title: {}}).then(function(){
203 | assert.fail();
204 | done();
205 | }).catch(function(){
206 | assert.ok(true);
207 | done();
208 | });
209 | });
210 | });
211 |
212 | describe('resolve', function(){
213 | it('should resolve URLs properly', function(){
214 | var url = 'https://soundcloud.com/dj-perun/its-a-deep-bark';
215 | SC.resolve(url);
216 | var requestUrl = decodeURIComponent(this.requests[0].url);
217 | var urlPart = requestUrl.split('url=')[1].split('&')[0];
218 | assert.ok(urlPart === url, 'The URL in the request should match the original URL');
219 | assert.ok(requestUrl.indexOf('&_status_code_map[302]=200') >= 0, 'Should be mapping a 302 to a 200');
220 | });
221 | });
222 | });
223 |
--------------------------------------------------------------------------------
/test/initialize-test.js:
--------------------------------------------------------------------------------
1 | describe('SDK initialize', function () {
2 | it('should make isConnected true when oauth_token is set', function () {
3 | SC.initialize({
4 | oauth_token: 'oauth_token'
5 | });
6 |
7 | assert.ok(SC.isConnected());
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/test/recorder-test.js:
--------------------------------------------------------------------------------
1 | var Recorder = SC.Recorder;
2 |
3 | var AudioContext = window.AudioContext || window.webkitAudioContext;
4 | var context;
5 | if (AudioContext) {
6 | context = new AudioContext();
7 | }
8 |
9 | var testRecorder = function(){
10 | return new Recorder({
11 | source: context.createBufferSource()
12 | });
13 | };
14 |
15 | // returns a silent oscillator
16 | var silentOscillator = function(){
17 | var osc = context.createOscillator();
18 | var gain = context.createGain();
19 | gain.gain.value = 0;
20 | osc.connect(gain);
21 | gain.connect(context.destination);
22 | return osc;
23 | };
24 |
25 | describe('Recorder', function () {
26 | if (!AudioContext) {
27 | console.log('Browser does not support the Web Audio API, will skip Recorder tests.');
28 | return;
29 | }
30 |
31 | it('should initialize properly', function () {
32 | var options = {
33 | context: {},
34 | source: {}
35 | };
36 |
37 | var recorder = new Recorder(options);
38 |
39 | assert.equal(recorder.context, options.context, 'uses the provided context');
40 | assert.equal(recorder.source, options.source, 'uses the provided source');
41 | });
42 |
43 | describe('start', function(){
44 | it('should start the recording', function(done){
45 | var recorder = testRecorder();
46 | return recorder.start().then(function(){
47 | assert.ok(recorder._recorder, 'instantiated the recorder');
48 | done();
49 | }).catch(function(err){
50 | done(err);
51 | });
52 | });
53 |
54 | it('should throw an error if the source is not valid', function(done){
55 | var recorder = new Recorder({
56 | source: {}
57 | });
58 | recorder.start().then(function(){
59 | assert.fail('It should not go to `then`');
60 | done()
61 | }).catch(function(err){
62 | assert.ok(true);
63 | done();
64 | });
65 | });
66 | });
67 |
68 | describe('recording', function(){
69 | it('should return a buffer', function(done){
70 | var osc = silentOscillator();
71 | var recorder = new Recorder({
72 | context: context,
73 | source: osc
74 | });
75 | osc.start(0);
76 |
77 | recorder.start().then(function(){
78 | setTimeout(function(){
79 | recorder.stop();
80 | osc.stop(0);
81 | recorder.getBuffer().then(function(buffer){
82 | assert.ok(buffer instanceof AudioBuffer, 'provides a valid buffer');
83 | done();
84 | }).catch(function(err){
85 | done(err);
86 | })
87 | }, 500);
88 | });
89 | });
90 |
91 | it('should return a wav', function(done){
92 | var osc = silentOscillator();
93 | var recorder = new Recorder({
94 | context: context,
95 | source: osc
96 | });
97 | osc.start(0);
98 |
99 | recorder.start().then(function(){
100 | setTimeout(function(){
101 | recorder.stop();
102 | osc.stop(0);
103 | recorder.getWAV().then(function(wav){
104 | assert.ok(wav instanceof Blob, 'provides a valid wav blob');
105 | done();
106 | }).catch(function(err){
107 | done(err);
108 | })
109 | }, 500);
110 | });
111 | });
112 |
113 | it('should fail if recording has not started', function(done){
114 | var osc = silentOscillator();
115 | var recorder = new Recorder({
116 | context: context,
117 | source: osc
118 | });
119 | osc.start(0);
120 |
121 | setTimeout(function(){
122 | recorder.stop();
123 | osc.stop(0);
124 | return recorder.getBuffer().catch(function(){
125 | done();
126 | });
127 | }, 500);
128 | });
129 | });
130 |
131 | describe('play', function(){
132 | it('should play the recording', function(done){
133 | var osc = silentOscillator();
134 | var recorder = new Recorder({
135 | context: context,
136 | source: osc
137 | });
138 | osc.start(0);
139 |
140 | recorder.start().then(function(){
141 | setTimeout(function(){
142 | recorder.stop();
143 | osc.stop(0);
144 | recorder.play().then(function(node){
145 | assert.ok(node instanceof AudioBufferSourceNode);
146 | done();
147 | }).catch(function(err){
148 | done(err);
149 | });
150 | }, 500);
151 | });
152 | });
153 | });
154 | });
155 |
--------------------------------------------------------------------------------
/test/stream-test.js:
--------------------------------------------------------------------------------
1 | describe('SDK streaming', function () {
2 | beforeEach(function () {
3 | this.xhr = sinon.useFakeXMLHttpRequest();
4 |
5 | this.requests = [];
6 | this.xhr.onCreate = function(xhr) {
7 | this.requests.push(xhr);
8 | }.bind(this);
9 |
10 | SC.initialize({
11 | client_id: 'YOUR_CLIENT_ID',
12 | redirect_uri: 'http://localhost:8080'
13 | });
14 | });
15 |
16 | afterEach(function() {
17 | this.xhr.restore();
18 | });
19 |
20 | it('should only add a secret token to the url if given', function () {
21 | var secretToken = '123';
22 | SC.stream('/tracks/cool-track');
23 | assert(this.requests[0].url.indexOf('secret_token') === -1);
24 |
25 | SC.stream('/tracks/cool-track', secretToken);
26 | assert(this.requests[1].url.indexOf('secret_token') !== -1);
27 | assert(this.requests[1].url.indexOf(secretToken) !== -1);
28 | });
29 |
30 | it('should create a player object', function(done){
31 | SC.stream('/tracks/cool-track').then(function(player){
32 | assert.ok(player);
33 | assert.ok(player.play);
34 | assert.ok(player.pause);
35 | done();
36 | }).catch(function(e){
37 | done(e);
38 | });
39 | this.requests[0].respond(200,
40 | { 'Content-Type': 'text/json' },
41 | '{"id": 123, "duration": 23}');
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/vendor/playback/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | SCAudio: require('@sc/scaudio'),
3 | SCAudioControllerHTML5Player: require('@sc/scaudio-controller-html5-player'),
4 | SCAudioControllerHLSMSEPlayer: require('@sc/scaudio-controller-hls-mse-player'),
5 | SCAudioPublicApiStreamURLRetriever: require('@sc/scaudio-public-api-stream-url-retriever'),
6 | MaestroCore: require('@sc/maestro-core'),
7 | MaestroLoaders: require('@sc/maestro-loaders')
8 | };
9 |
--------------------------------------------------------------------------------
/vendor/playback/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 |
3 | module.exports = {
4 | entry: './index.js',
5 | output: {
6 | libraryTarget: 'commonjs2',
7 | filename: 'playback.js'
8 | },
9 | plugins: [ new webpack.optimize.UglifyJsPlugin() ]
10 | };
11 |
--------------------------------------------------------------------------------
/vendor/recorderjs/recorder.js:
--------------------------------------------------------------------------------
1 | var RecorderWorker = require('./recorder.worker');
2 |
3 | var Recorder = function(source, cfg){
4 | var config = cfg || {};
5 | var bufferLen = config.bufferLen || 4096;
6 | var numChannels = config.numChannels || 2;
7 | this.context = source.context;
8 | this.node = (this.context.createScriptProcessor ||
9 | this.context.createJavaScriptNode).call(this.context,
10 | bufferLen, numChannels, numChannels);
11 |
12 | var worker = new RecorderWorker();
13 | worker.postMessage({
14 | command: 'init',
15 | config: {
16 | sampleRate: this.context.sampleRate,
17 | numChannels: numChannels
18 | }
19 | });
20 | var recording = false,
21 | currCallback;
22 |
23 | this.node.onaudioprocess = function(e){
24 | if (!recording) return;
25 | var buffer = [];
26 | for (var channel = 0; channel < numChannels; channel++){
27 | buffer.push(e.inputBuffer.getChannelData(channel));
28 | }
29 | worker.postMessage({
30 | command: 'record',
31 | buffer: buffer
32 | });
33 | }
34 |
35 | this.configure = function(cfg){
36 | for (var prop in cfg){
37 | if (cfg.hasOwnProperty(prop)){
38 | config[prop] = cfg[prop];
39 | }
40 | }
41 | }
42 |
43 | this.record = function(){
44 | recording = true;
45 | }
46 |
47 | this.stop = function(){
48 | recording = false;
49 | }
50 |
51 | this.clear = function(){
52 | worker.postMessage({ command: 'clear' });
53 | }
54 |
55 | this.getBuffer = function(cb) {
56 | currCallback = cb || config.callback;
57 | worker.postMessage({ command: 'getBuffer' })
58 | }
59 |
60 | this.exportWAV = function(cb, type){
61 | currCallback = cb || config.callback;
62 | type = type || config.type || 'audio/wav';
63 | if (!currCallback) throw new Error('Callback not set');
64 | worker.postMessage({
65 | command: 'exportWAV',
66 | type: type
67 | });
68 | }
69 |
70 | worker.onmessage = function(e){
71 | var blob = e.data;
72 | currCallback(blob);
73 | }
74 |
75 | source.connect(this.node);
76 | this.node.connect(this.context.destination); //this should not be necessary
77 | };
78 |
79 | Recorder.forceDownload = function(blob, filename){
80 | var url = (window.URL || window.webkitURL).createObjectURL(blob);
81 | var link = window.document.createElement('a');
82 | link.href = url;
83 | link.download = filename || 'output.wav';
84 | var click = document.createEvent('Event');
85 | click.initEvent('click', true, true);
86 | link.dispatchEvent(click);
87 | }
88 |
89 | module.exports = Recorder;
90 |
--------------------------------------------------------------------------------
/vendor/recorderjs/recorder.worker.js:
--------------------------------------------------------------------------------
1 | var recLength = 0,
2 | recBuffers = [],
3 | sampleRate,
4 | numChannels;
5 |
6 | global.onmessage = function(e){
7 | switch (e.data.command){
8 | case 'init':
9 | init(e.data.config);
10 | break;
11 | case 'record':
12 | record(e.data.buffer);
13 | break;
14 | case 'exportWAV':
15 | exportWAV(e.data.type);
16 | break;
17 | case 'getBuffer':
18 | getBuffer();
19 | break;
20 | case 'clear':
21 | clear();
22 | break;
23 | }
24 | };
25 |
26 | function init(config){
27 | sampleRate = config.sampleRate;
28 | numChannels = config.numChannels;
29 | initBuffers();
30 | }
31 |
32 | function record(inputBuffer){
33 | for (var channel = 0; channel < numChannels; channel++){
34 | recBuffers[channel].push(inputBuffer[channel]);
35 | }
36 | recLength += inputBuffer[0].length;
37 | }
38 |
39 | function exportWAV(type){
40 | var buffers = [];
41 | for (var channel = 0; channel < numChannels; channel++){
42 | buffers.push(mergeBuffers(recBuffers[channel], recLength));
43 | }
44 | if (numChannels === 2){
45 | var interleaved = interleave(buffers[0], buffers[1]);
46 | } else {
47 | var interleaved = buffers[0];
48 | }
49 | var dataview = encodeWAV(interleaved);
50 | var audioBlob = new Blob([dataview], { type: type });
51 |
52 | this.postMessage(audioBlob);
53 | }
54 |
55 | function getBuffer(){
56 | var buffers = [];
57 | for (var channel = 0; channel < numChannels; channel++){
58 | buffers.push(mergeBuffers(recBuffers[channel], recLength));
59 | }
60 | this.postMessage(buffers);
61 | }
62 |
63 | function clear(){
64 | recLength = 0;
65 | recBuffers = [];
66 | initBuffers();
67 | }
68 |
69 | function initBuffers(){
70 | for (var channel = 0; channel < numChannels; channel++){
71 | recBuffers[channel] = [];
72 | }
73 | }
74 |
75 | function mergeBuffers(recBuffers, recLength){
76 | var result = new Float32Array(recLength);
77 | var offset = 0;
78 | for (var i = 0; i < recBuffers.length; i++){
79 | result.set(recBuffers[i], offset);
80 | offset += recBuffers[i].length;
81 | }
82 | return result;
83 | }
84 |
85 | function interleave(inputL, inputR){
86 | var length = inputL.length + inputR.length;
87 | var result = new Float32Array(length);
88 |
89 | var index = 0,
90 | inputIndex = 0;
91 |
92 | while (index < length){
93 | result[index++] = inputL[inputIndex];
94 | result[index++] = inputR[inputIndex];
95 | inputIndex++;
96 | }
97 | return result;
98 | }
99 |
100 | function floatTo16BitPCM(output, offset, input){
101 | for (var i = 0; i < input.length; i++, offset+=2){
102 | var s = Math.max(-1, Math.min(1, input[i]));
103 | output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
104 | }
105 | }
106 |
107 | function writeString(view, offset, string){
108 | for (var i = 0; i < string.length; i++){
109 | view.setUint8(offset + i, string.charCodeAt(i));
110 | }
111 | }
112 |
113 | function encodeWAV(samples){
114 | var buffer = new ArrayBuffer(44 + samples.length * 2);
115 | var view = new DataView(buffer);
116 |
117 | /* RIFF identifier */
118 | writeString(view, 0, 'RIFF');
119 | /* RIFF chunk length */
120 | view.setUint32(4, 36 + samples.length * 2, true);
121 | /* RIFF type */
122 | writeString(view, 8, 'WAVE');
123 | /* format chunk identifier */
124 | writeString(view, 12, 'fmt ');
125 | /* format chunk length */
126 | view.setUint32(16, 16, true);
127 | /* sample format (raw) */
128 | view.setUint16(20, 1, true);
129 | /* channel count */
130 | view.setUint16(22, numChannels, true);
131 | /* sample rate */
132 | view.setUint32(24, sampleRate, true);
133 | /* byte rate (sample rate * block align) */
134 | view.setUint32(28, sampleRate * 4, true);
135 | /* block align (channel count * bytes per sample) */
136 | view.setUint16(32, numChannels * 2, true);
137 | /* bits per sample */
138 | view.setUint16(34, 16, true);
139 | /* data chunk identifier */
140 | writeString(view, 36, 'data');
141 | /* data chunk length */
142 | view.setUint32(40, samples.length * 2, true);
143 |
144 | floatTo16BitPCM(view, 44, samples);
145 |
146 | return view;
147 | }
148 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const VERSION = require('./package.json').version;
2 | const IS_NPM = process.env.IS_NPM;
3 | const path = IS_NPM ? __dirname : __dirname + '/build/sdk';
4 | const filename = IS_NPM ? 'sdk.js' : 'sdk-' + VERSION + '.js';
5 |
6 | module.exports = {
7 | entry: './index.js',
8 | output: {
9 | path: path,
10 | filename: filename,
11 | libraryTarget: 'umd'
12 | },
13 | module: {
14 | loaders: [{
15 | test: /\.js?$/,
16 | exclude: /(node_modules|vendor)/,
17 | loader: 'babel-loader',
18 | query: {
19 | cacheDirectory: true
20 | }
21 | },{
22 | test: /\.worker\.js?$/,
23 | exclude: /(node_modules)/,
24 | loader: 'worker-loader?inline=true',
25 | query: {
26 | inline: true
27 | }
28 | }]
29 | },
30 | devServer: {
31 | contentBase: './examples'
32 | },
33 | devtool: 'source-map'
34 | };
35 |
--------------------------------------------------------------------------------