Skip to content

Commit

Permalink
Merge pull request #81 from soundcloud/update-playback-library
Browse files Browse the repository at this point in the history
update playback library to maestro
  • Loading branch information
tjenkinson authored Aug 17, 2017
2 parents 648396a + 3147db4 commit 596d70e
Show file tree
Hide file tree
Showing 12 changed files with 206 additions and 64 deletions.
33 changes: 14 additions & 19 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@ DEP := vendor

NODE_VERSION := 6.11.1
NODE := nodejs-$(NODE_VERSION)
NODE_BIN := $(DESTBIN)/node
NPM_BIN := $(DESTBIN)/npm

NPM_REGISTRY := http://npm.dev.s-cloud.net

export PATH := $(DESTBIN):$(NM_BIN):$(PATH)

.PHONY: build sc-vendor-libs test run publish dirs clean sc-vendor-libs
.PHONY: setup build sc-vendor-libs test run publish dirs clean

node_modules: $(NPM_BIN) package.json
$(NPM_BIN) install
@touch $@

setup: $(NPM_BIN)

build: node_modules
$(NPM_BIN) run build

Expand All @@ -41,32 +42,26 @@ publish: test
dirs:
echo $(DESTDIR)
echo $(DESTBIN)
echo $(NODE_BIN)
echo $(NPM_BIN)

clean:
rm -rf $(NODE_MODULES) $(BUILD_DIR)/* sdk.js $(DEP)/node

vendor/audiomanager.js:
$(NPM_BIN) install @sc/audiomanager --registry=$(NPM_REGISTRY)
cp $(NODE_MODULES)/@sc/audiomanager/build/audiomanager.min.js vendor/audiomanager.js

vendor/scaudio.js:
$(NPM_BIN) install @sc/scaudio --registry=$(NPM_REGISTRY)
cp $(NODE_MODULES)/@sc/scaudio/scaudio.min.js vendor/scaudio.js

sc-vendor-libs: vendor/audiomanager.js vendor/scaudio.js
rm -rf $(NODE_MODULES) $(BUILD_DIR)/* $(TMP) $(DEP)/node sdk.js vendor/playback/playback.js

sc-vendor-libs: node_modules
$(NPM_BIN) install --registry=$(NPM_REGISTRY) \
@sc/scaudio \
@sc/scaudio-public-api-stream-url-retriever \
@sc/maestro-core \
@sc/maestro-loaders \
@sc/maestro-html5-player \
@sc/maestro-hls-mse-player
$(NPM_BIN) run buildPlayback

$(NPM_BIN): $(DESTDIR)/usr/lib/$(NODE)/bin/node
@mkdir -p $(@D)
ln -sf $(DESTDIR)/usr/lib/$(NODE)/bin/npm $@
@touch $@

$(NODE_BIN): $(DESTDIR)/usr/lib/$(NODE)/bin/node
@mkdir -p $(@D)
ln -sf $< $@
@touch $@

$(DESTDIR)/usr/lib/$(NODE)/bin/node: $(DEP)/node/$(OS)/$(NODE_VERSION).tar.gz
@mkdir -p $(@D)
tar xz -C $(DESTDIR)/usr/lib/$(NODE) --strip-components 1 -f $<
Expand Down
Binary file removed examples/flashAudio.swf
Binary file not shown.
23 changes: 15 additions & 8 deletions examples/streaming.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,27 @@
var info = document.getElementById('info');
var currentPlayer;

function play() {
if (currentPlayer) {
currentPlayer.play().then(function(){
console.log('Playback started.');
}).catch(function(e){
console.error('Playback rejected.', e);
});
}
}

var streamTrack = function(track){
return SC.stream('/tracks/' + track.id).then(function(player){
title.innerText = track.title;
info.style.display = 'inline-block';
if (currentPlayer) {
currentPlayer.pause();
}
currentPlayer = player;

player.play();
}).catch(function(){
console.log(arguments);
currentPlayer = window.player = player;
play();
}).catch(function(e){
console.error(e);
});
};

Expand All @@ -82,9 +91,7 @@
}
});
document.getElementById('play').addEventListener('click', function(){
if (currentPlayer) {
currentPlayer.play();
}
play();
});
</script>
</body>
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"scripts": {
"test": "webpack && karma start --single-run",
"build": "webpack -p",
"buildPlayback": "cd ./vendor/playback && webpack --config webpack.config.js",
"start": "IS_NPM=1 webpack-dev-server",
"serve": "node ./serve.js"
},
Expand All @@ -35,6 +36,7 @@
"worker-loader": "^0.6.0"
},
"dependencies": {
"backbone-events-standalone": "^0.2.7",
"es6-promise": "^2.3.0",
"form-urlencoded": "^0.1.4",
"node-static": "^0.7.7",
Expand Down
93 changes: 93 additions & 0 deletions src/player-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
const BackboneEvents = require('backbone-events-standalone');
const { errors: { PlayerFatalError }, State } = require('../vendor/playback/playback').MaestroCore;
const { errors: { GeoBlockedError, NoStreamsError, TimedOutError, NotSupportedError } } = require('../vendor/playback/playback').SCAudio;

const TIMEUPDATE_INTERVAL = 1000 / 60;

module.exports = function(scaudioPlayer) {
function getState() {
switch (scaudioPlayer.getState()) {
case State.PLAYING:
return 'playing';
case State.PAUSED:
return scaudioPlayer.isEnded() ? 'ended' : 'paused';
case State.DEAD:
return scaudioPlayer.getFatalError() ? 'error' : 'dead';
case State.LOADING:
default:
return 'loading';
}
}

function handleEmittingTimeEvents() {
let timerId = 0;
let previousPosition = null;
scaudioPlayer.onChange.subscribe(({ playing, seeking, dead }) => {
if (dead) {
window.clearTimeout(timerId);
} else if (playing !== undefined || seeking !== undefined) {
doEmit();
}
});
function doEmit() {
window.clearTimeout(timerId);
if (scaudioPlayer.isPlaying() && !scaudioPlayer.isEnded()) {
timerId = window.setTimeout(doEmit, TIMEUPDATE_INTERVAL);
}
const newPosition = scaudioPlayer.getPosition();
if (newPosition !== previousPosition) {
previousPosition = newPosition;
playerApi.trigger('time', newPosition);
}
}
}
let hadFirstPlay = false;
scaudioPlayer.onStateChange.subscribe(() => playerApi.trigger('state-change', getState()));
scaudioPlayer.onPlay.subscribe(() => {
playerApi.trigger(hadFirstPlay ? 'play-resume' : 'play-start');
hadFirstPlay = true;
});

scaudioPlayer.onPlayIntent.subscribe(() => playerApi.trigger('play'));
scaudioPlayer.onPlayRejection.subscribe((playRejection) => playerApi.trigger('play-rejection', playRejection));
scaudioPlayer.onPauseIntent.subscribe(() => playerApi.trigger('pause'));
scaudioPlayer.onSeek.subscribe(() => playerApi.trigger('seeked'));
scaudioPlayer.onSeekRejection.subscribe((seekRejection) => playerApi.trigger('seek-rejection', seekRejection));
scaudioPlayer.onLoadStart.subscribe(() => playerApi.trigger('buffering_start'));
scaudioPlayer.onLoadEnd.subscribe(() => playerApi.trigger('buffering_end'));
scaudioPlayer.onEnded.subscribe(() => playerApi.trigger('finish'));
scaudioPlayer.onError.subscribe((error) => {
if (error instanceof GeoBlockedError) {
playerApi.trigger('geo_blocked');
} else if (error instanceof NoStreamsError) {
playerApi.trigger('no_streams');
} else if (error instanceof TimedOutError) {
playerApi.trigger('no_connection');
} else if (error instanceof NotSupportedError) {
playerApi.trigger('no_protocol');
} else if (error instanceof PlayerFatalError) {
playerApi.trigger('audio_error');
}
});

const playerApi = {
play: scaudioPlayer.play.bind(scaudioPlayer),
pause: scaudioPlayer.pause.bind(scaudioPlayer),
seek: scaudioPlayer.seek.bind(scaudioPlayer),
getVolume: scaudioPlayer.getVolume.bind(scaudioPlayer),
setVolume: scaudioPlayer.setVolume.bind(scaudioPlayer),
currentTime: scaudioPlayer.getPosition.bind(scaudioPlayer),
getDuration: scaudioPlayer.getDuration.bind(scaudioPlayer),
isBuffering: scaudioPlayer.isLoading.bind(scaudioPlayer),
isPlaying: scaudioPlayer.isPlaying.bind(scaudioPlayer),
isActuallyPlaying: scaudioPlayer.isActuallyPlaying.bind(scaudioPlayer),
isEnded: scaudioPlayer.isEnded.bind(scaudioPlayer),
isDead: scaudioPlayer.isDead.bind(scaudioPlayer),
kill: scaudioPlayer.kill.bind(scaudioPlayer),
hasErrored: () => !!scaudioPlayer.getFatalError(),
getState
};
BackboneEvents.mixin(playerApi);
handleEmittingTimeEvents();
return playerApi;
}
65 changes: 48 additions & 17 deletions src/stream.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
const api = require('./api');
const AudioManager = require('../vendor/audiomanager');
const audioManager = new AudioManager({
flashAudioPath: 'https://connect.soundcloud.com/sdk/flashAudio.swf'
});
const config = require('./config');
const SCAudio = require('../vendor/scaudio');
const playerApi = require('./player-api');
const SCAudio = require('../vendor/playback/playback').SCAudio;
const StreamUrlRetriever = require('../vendor/playback/playback').SCAudioPublicApiStreamURLRetriever.StreamUrlRetriever;
const MaestroHTML5Player = require('../vendor/playback/playback').MaestroHTML5Player.HTML5Player;
const MaestroHLSMSEPlayer = require('../vendor/playback/playback').MaestroHLSMSEPlayer.HLSMSEPlayer;
const stringLoader = require('../vendor/playback/playback').MaestroLoaders.stringLoader;

const SNIPPET_FADEOUT = 3000; // ms

/**
* Fetches track info and instantiates a player for the track
Expand All @@ -16,22 +19,50 @@ module.exports = (trackPath, secretToken) => {
const options = secretToken ? {secret_token: secretToken} : {};

return api.request('GET', trackPath, options).then((track) => {
function registerPlay() {
let registerEndpoint = `${baseURL}/tracks/${encodeURIComponent(track.id)}/plays?client_id=${encodeURIComponent(clientId)}`;
if (secretToken) {
registerEndpoint += `&secret_token=${encodeURIComponent(secretToken)}`;
}
const xhr = new XMLHttpRequest();
xhr.open('POST', registerEndpoint, true);
xhr.send();
}

const baseURL = config.get('baseURL')
const clientId = config.get('client_id');
const oauthToken = config.get('oauth_token');

let streamsEndpoint = `${baseURL}/tracks/${track.id}/streams?client_id=${clientId}`;
let registerEndpoint = `${baseURL}/tracks/${track.id}/plays?client_id=${clientId}`;

if (secretToken) {
streamsEndpoint += `&secret_token=${secretToken}`;
registerEndpoint += `&secret_token=${secretToken}`;
}
let playRegistered = false;
const streamUrlRetriever = new StreamUrlRetriever({
clientId,
secretToken,
trackId: track.id,
requestAuthorization: oauthToken ? 'OAuth ' + oauthToken : null,
loader: stringLoader
});

return new SCAudio(audioManager, {
soundId: track.id,
duration: track.duration,
streamUrlsEndpoint: streamsEndpoint,
registerEndpoint: registerEndpoint
const player = SCAudio.buildPlayer({
playerClasses: [ MaestroHTML5Player, MaestroHLSMSEPlayer ],
streamUrlRetriever,
fadeOutDuration: track.policy === 'SNIP' ? SNIPPET_FADEOUT : 0
});
player.onPlay.subscribe(() => {
if (!playRegistered) {
playRegistered = true;
registerPlay();
}
});
player.onEnded.subscribe(() => {
// maestro keeps the old playing state when at the end. Call pause() to maintain backwards compatibility
player.pause();
});
player.onPlayIntent.subscribe(() => {
if (player.isEnded()) {
// seek back to 0 if the user calls play() and we're at the end.
player.seek(0);
}
});
return playerApi(player);
});
};
2 changes: 1 addition & 1 deletion test/stream-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@ describe('SDK streaming', function () {
});
this.requests[0].respond(200,
{ 'Content-Type': 'text/json' },
'{"id": "123", "duration": 23}');
'{"id": 123, "duration": 23}');
});
});
13 changes: 0 additions & 13 deletions vendor/audiomanager.js

This file was deleted.

8 changes: 8 additions & 0 deletions vendor/playback/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
SCAudio: require('@sc/scaudio'),
SCAudioPublicApiStreamURLRetriever: require('@sc/scaudio-public-api-stream-url-retriever'),
MaestroCore: require('@sc/maestro-core'),
MaestroLoaders: require('@sc/maestro-loaders'),
MaestroHTML5Player: require('@sc/maestro-html5-player'),
MaestroHLSMSEPlayer: require('@sc/maestro-hls-mse-player')
};
15 changes: 15 additions & 0 deletions vendor/playback/playback.js

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions vendor/playback/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const webpack = require('webpack');

module.exports = {
entry: './index.js',
output: {
libraryTarget: 'commonjs2',
filename: 'playback.js'
},
plugins: [ new webpack.optimize.UglifyJsPlugin() ]
};
Loading

0 comments on commit 596d70e

Please sign in to comment.