UNPKG

streamium

Version:

Decentralized trustless video streaming using bitcoin payment channels.

277 lines (227 loc) 8.47 kB
'use strict'; angular.module('streamium.client.service', []) .service('StreamiumClient', function(bitcore, channel, Insight, events, inherits, Duration, PeerJS) { var Consumer = channel.Consumer; var SECONDS_IN_MINUTE = 60; var MILLIS_IN_SECOND = 1000; var MILLIS_IN_MINUTE = MILLIS_IN_SECOND * SECONDS_IN_MINUTE; var TIMESTEP = 10 * MILLIS_IN_SECOND; function StreamiumClient() { this.network = bitcore.Networks.get(config.network); bitcore.Networks.defaultNetwork = this.network; this.fundingKey = localStorage.getItem('fundingKey'); if (!this.fundingKey) { this.fundingKey = new bitcore.PrivateKey(); localStorage.setItem('fundingKey', this.fundingKey.toString()); } else { this.fundingKey = new bitcore.PrivateKey(this.fundingKey); } this.rate = this.providerKey = null; this.peer = this.connection = null; this.config = PeerJS.primary; events.EventEmitter.call(this); } inherits(StreamiumClient, events.EventEmitter); StreamiumClient.STATUS = { disconnected: 'disconnected', connecting: 'connecting', funding: 'funding', ready: 'ready', waiting: 'waiting', finished: 'finished' }; StreamiumClient.prototype.connect = function(streamId, callback) { this.streamId = streamId; this.peer = new Peer(null, this.config); this.status = StreamiumClient.STATUS.connecting; this.fundingCallback = callback; var self = this; this.peer.on('open', function onOpen(connection) { console.log('Connected to peer server:', self.peer); self.connection = self.peer.connect(streamId); self.connection.on('open', function() { self.connection.send({ type: 'hello', payload: '' }); self.connection.on('data', function(data) { // console.log('New message:', data); if (!data.type || !self.handlers[data.type]) throw 'Kernel panic'; // TODO! self.handlers[data.type].call(self, data.payload); }); }); }); this.peer.on('close', function onClose() { console.log('Provider connection closed'); self.status = StreamiumClient.STATUS.finished; self.end(); }); this.peer.on('error', function onError(error) { console.log('Error with provider connection', error); if (self.canFallback()) { self.config = PeerJS.secondary; self.connect(streamId, callback); } else { self.status = StreamiumClient.STATUS.disconnected; callback(error); } }); }; StreamiumClient.prototype.canFallback = function (argument) { return this.config == PeerJS.primary && this.status == StreamiumClient.STATUS.connecting; }; StreamiumClient.prototype.handlers = {}; StreamiumClient.prototype.handlers.end = function() { // provider is letting us know that he broadcasted the payment this.errored = false; }; StreamiumClient.prototype.handlers.hello = function(data) { if (this.status !== StreamiumClient.STATUS.connecting) { console.log('Error: Received `hello` when status is ', this.status, ':', data); return; } if (!('rate' in data && 'publicKey' in data && 'paymentAddress' in data)) { console.log('Error: Malformed data payload in hello handler:', data); return; } this.errored = true; this.rate = data.rate; this.stepSatoshis = Math.round( TIMESTEP * bitcore.Unit.fromBTC(this.rate).toSatoshis() / MILLIS_IN_MINUTE ); this.providerKey = data.publicKey; this.providerAddress = new bitcore.Address(data.paymentAddress); this.status = StreamiumClient.STATUS.funding; this.isStatic = data.isStatic; this.consumer = new Consumer({ network: this.network, providerPublicKey: this.providerKey, providerAddress: this.providerAddress, refundAddress: this.refundAddress || this.fundingKey.toAddress(), fundingKey: this.fundingKey }); this.fundingCallback(null, this.consumer.fundingAddress.toString(), this.isStatic); this.fundingCallback = null; }; StreamiumClient.prototype.setRelayParams = function(params) { // Hack: testnet not working params.address = new bitcore.Address(new bitcore.Address(params.address).hashBuffer).toString() this.relayParams = params; }; StreamiumClient.prototype.processFunding = function(utxos) { this.consumer.processFunding(utxos); }; StreamiumClient.prototype.handlers.video = function(data) { this.onStream.apply(self, arguments); }; StreamiumClient.prototype.onStream = function() { console.log('No stream handler'); }; StreamiumClient.prototype.handlers.refundAck = function(data) { if (this.status !== StreamiumClient.STATUS.waiting) { console.log('Error: received refundAck message when status is: ', this.status); return; } var self = this; if (typeof data === 'string') { data = JSON.parse(data); } this.consumer.validateRefund(data); this.status = StreamiumClient.STATUS.ready; localStorage.setItem('refund_' + this.streamId + '_' + this.consumer.refundTx.nLockTime, this.consumer.refundTx.toString()); $.ajax({ url: config.RELAYSTORE_POST, method: 'post', data: this.consumer.refundTx.toString() }); self.emit('refundReceived'); }; StreamiumClient.prototype.handlers.message = function(data) { this.emit('chatroom:message', data); }; StreamiumClient.prototype.getDuration = function(satoshis) { return Duration.for(this.rate, satoshis); }; StreamiumClient.prototype.getExpirationDate = function() { return new Date(this.startTime + this.getDuration(this.consumer.refundTx.outputAmount)); }; StreamiumClient.prototype.sendFirstPayment = function() { // The time window to establish so the provider doesn't cut the channel. // Currently two times the step between payments (20 seconds) var satoshis = 2 * this.stepSatoshis; this.startTime = new Date().getTime(); this.sendCommitment(); this.consumer.incrementPaymentBy(satoshis); this.sendPayment(); }; StreamiumClient.prototype.setupPaymentUpdates = function() { var self = this; this.startTime = new Date().getTime(); this.interval = setInterval(function() { self.updatePayment(); }, TIMESTEP); }; StreamiumClient.prototype.updatePayment = function() { var maxSatoshis = this.consumer.refundTx.outputAmount; var currentSatoshis = this.consumer.paymentTx.paid + this.consumer.paymentTx.getFee() + this.stepSatoshis; if (currentSatoshis > maxSatoshis) { return; } console.log('used', currentSatoshis, 'of', maxSatoshis, 'satoshis'); this.consumer.incrementPaymentBy(this.stepSatoshis); this.sendPayment(); }; StreamiumClient.prototype.sendPayment = function() { if (this.status !== StreamiumClient.STATUS.ready) { console.log('Error: not ready to pay! Status is: ', this.status); return; } this.connection.send({ type: 'payment', payload: this.consumer.paymentTx.toJSON() }); this.emit('paymentUpdate'); }; StreamiumClient.prototype.sendCommitment = function() { this.connection.send({ type: 'commitment', payload: this.consumer.commitmentTx.toJSON() }); }; StreamiumClient.prototype.handlers.paymentAck = function() { // TODO: Pass }; StreamiumClient.prototype.isReady = function() { return !!this.consumer; }; StreamiumClient.prototype.sendMessage = function(message) { this.connection.send({ type: 'message', payload: message }); }; StreamiumClient.prototype.askForRefund = function() { if (this.status !== StreamiumClient.STATUS.funding) { console.log('Error: trying to ask for refund tx when status is: ', this.status); return; } bitcore.util.preconditions.checkState( this.consumer.commitmentTx.inputAmount > 0, 'Transaction is not funded' ); this.status = StreamiumClient.STATUS.waiting; var payload = this.consumer.setupRefund(this.relayParams).toJSON(); this.connection.send({ type: 'sign', payload: payload }); }; StreamiumClient.prototype.end = function() { console.log('clearing interval ' + this.interval); clearInterval(this.interval); this.status = StreamiumClient.STATUS.finished; this.emit('end'); }; return new StreamiumClient(); });