diff --git a/.gitignore b/.gitignore index f06235c..a56a7ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ node_modules -dist + diff --git a/dist/Meta.d.ts b/dist/Meta.d.ts new file mode 100644 index 0000000..6a2c979 --- /dev/null +++ b/dist/Meta.d.ts @@ -0,0 +1,18 @@ +export declare const ControlHeaders: { + FILE_START: number; + FILE_CHUNK: number; + FILE_CHUNK_ACK: number; + FILE_END: number; + TRANSFER_PAUSE: number; + TRANSFER_RESUME: number; + TRANSFER_CANCEL: number; +}; +export interface FileSendRequest { + filename: string; + filesizeBytes: number; +} +export interface FileStartMetadata { + fileName: string; + fileSize: number; + fileType: string; +} diff --git a/dist/Meta.js b/dist/Meta.js new file mode 100644 index 0000000..64e9ed2 --- /dev/null +++ b/dist/Meta.js @@ -0,0 +1,13 @@ +"use strict"; +// The first byte in every data sent will be what kind of data it is +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ControlHeaders = void 0; +exports.ControlHeaders = { + FILE_START: 0, + FILE_CHUNK: 1, + FILE_CHUNK_ACK: 2, + FILE_END: 3, + TRANSFER_PAUSE: 4, + TRANSFER_RESUME: 5, + TRANSFER_CANCEL: 6 +}; diff --git a/dist/PeerFileReceive.d.ts b/dist/PeerFileReceive.d.ts new file mode 100644 index 0000000..7ff66f1 --- /dev/null +++ b/dist/PeerFileReceive.d.ts @@ -0,0 +1,37 @@ +import { Readable } from 'readable-stream'; +import { EventEmitter } from 'ee-ts'; +import SimplePeer from 'simple-peer'; +interface Events { + progress(percentage: number, bytesSent: number): void; + done(receivedFile: File): void; + pause(): void; + paused(): void; + resume(): void; + cancel(): void; + cancelled(): void; +} +export default class PeerFileReceive extends EventEmitter { + paused: boolean; + cancelled: boolean; + bytesReceived: number; + peer: SimplePeer.Instance; + private rs; + fileName: string; + fileSize: number; + private fileData; + private fileStream; + fileType: string; + constructor(peer: SimplePeer.Instance); + setPeer(peer: SimplePeer.Instance): void; + /** + * Send a message to sender + * @param header Type of message + * @param data Message + */ + private sendPeer; + createReadStream(): Readable; + pause(): void; + resume(): void; + cancel(): void; +} +export {}; diff --git a/dist/PeerFileReceive.js b/dist/PeerFileReceive.js new file mode 100644 index 0000000..467c4b9 --- /dev/null +++ b/dist/PeerFileReceive.js @@ -0,0 +1,159 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +var readable_stream_1 = require("readable-stream"); +var ee_ts_1 = require("ee-ts"); +var Meta_1 = require("./Meta"); +var ReceiveStream = /** @class */ (function (_super) { + __extends(ReceiveStream, _super); + function ReceiveStream() { + return _super !== null && _super.apply(this, arguments) || this; + } + /** + * File stream writes here + * @param chunk + * @param encoding + * @param cb + */ + ReceiveStream.prototype._write = function (data, encoding, cb) { + if (data[0] === Meta_1.ControlHeaders.FILE_START) { + var meta = JSON.parse(new TextDecoder().decode(data.slice(1))); + this.emit('start', meta); + } + else if (data[0] === Meta_1.ControlHeaders.FILE_CHUNK) { + this.emit('chunk', data.slice(1)); + } + else if (data[0] === Meta_1.ControlHeaders.TRANSFER_PAUSE) { + this.emit('paused'); + } + if (data[0] === Meta_1.ControlHeaders.TRANSFER_CANCEL) { + this.emit('cancelled'); + this.destroy(); + } + else { + cb(null); // Signal that we're ready for more data + } + }; + return ReceiveStream; +}(readable_stream_1.Writable)); +var PeerFileReceive = /** @class */ (function (_super) { + __extends(PeerFileReceive, _super); + function PeerFileReceive(peer) { + var _this = _super.call(this) || this; + _this.paused = false; + _this.cancelled = false; + _this.bytesReceived = 0; + _this.fileData = []; + _this.fileStream = null; + _this.setPeer(peer); + return _this; + } + // When peer is changed, start a new stream handler and assign events + PeerFileReceive.prototype.setPeer = function (peer) { + var _this = this; + if (this.rs) { + this.rs.destroy(); + } + this.rs = new ReceiveStream(); + this.peer = peer; + peer.pipe(this.rs); + this.rs.on('start', function (meta) { + _this.fileName = meta.fileName; + _this.fileSize = meta.fileSize; + _this.fileType = meta.fileType; + _this.fileData = []; + }); + this.rs.on('chunk', function (chunk) { + _this.fileData.push(chunk); + if (_this.fileStream) { + _this.fileStream.push(chunk); + } + _this.bytesReceived += chunk.byteLength; + if (_this.bytesReceived === _this.fileSize) { + // completed + _this.sendPeer(Meta_1.ControlHeaders.FILE_END); + if (_this.fileStream) + _this.fileStream.push(null); // EOF + var file = new window.File(_this.fileData, _this.fileName, { + type: _this.fileType + }); + _this.emit('progress', 100.0, _this.fileSize); + _this.emit('done', file); + } + else { + var percentage = parseFloat((100 * (_this.bytesReceived / _this.fileSize)).toFixed(3)); + _this.emit('progress', percentage, _this.bytesReceived); + } + }); + this.rs.on('paused', function () { + _this.emit('paused'); + }); + this.rs.on('cancelled', function () { + _this.emit('cancelled'); + }); + }; + /** + * Send a message to sender + * @param header Type of message + * @param data Message + */ + PeerFileReceive.prototype.sendPeer = function (header, data) { + if (data === void 0) { data = null; } + if (!this.peer.connected) + return; + var resp; + if (data) { + resp = new Uint8Array(1 + data.length); + resp.set(data, 1); + } + else { + resp = new Uint8Array(1); + } + resp[0] = header; + this.peer.send(resp); + }; + // Create a stream for receiving file data + PeerFileReceive.prototype.createReadStream = function () { + this.fileStream = new readable_stream_1.Readable({ + objectMode: true, + read: function () { } // We'll be using push when we have file chunk + }); + return this.fileStream; + }; + // Request sender to pause transfer + PeerFileReceive.prototype.pause = function () { + this.sendPeer(Meta_1.ControlHeaders.TRANSFER_PAUSE); + this.paused = true; + this.emit('pause'); + }; + // Request sender to resume sending file + PeerFileReceive.prototype.resume = function () { + this.sendPeer(Meta_1.ControlHeaders.TRANSFER_RESUME); + this.paused = false; + this.emit('resume'); + }; + PeerFileReceive.prototype.cancel = function () { + this.cancelled = true; + this.sendPeer(Meta_1.ControlHeaders.TRANSFER_CANCEL); + this.fileData = []; + this.rs.destroy(); + this.peer.destroy(); + if (this.fileStream) + this.fileStream.destroy(); + this.emit('cancel'); + }; + return PeerFileReceive; +}(ee_ts_1.EventEmitter)); +exports.default = PeerFileReceive; diff --git a/dist/PeerFileSend.d.ts b/dist/PeerFileSend.d.ts new file mode 100644 index 0000000..978b8b9 --- /dev/null +++ b/dist/PeerFileSend.d.ts @@ -0,0 +1,42 @@ +import { EventEmitter } from 'ee-ts'; +import SimplePeer from 'simple-peer'; +interface Events { + progress(percentage: number, bytesSent: number): void; + done(): void; + pause(): void; + paused(): void; + resume(): void; + resumed(): void; + cancel(): void; + cancelled(): void; +} +export default class PeerFileSend extends EventEmitter { + paused: boolean; + cancelled: boolean; + receiverPaused: boolean; + peer: SimplePeer.Instance; + file: File; + private ss; + private offset; + /** + * @param peer Peer to send + * @param file File to send + * @param offset Bytes to start sending from, useful for file resume + */ + constructor(peer: SimplePeer.Instance, file: File, offset?: number); + /** + * Send a message to receiver + * @param header Type of message + * @param data Message + */ + private sendPeer; + private sendFileStartData; + setPeer(peer: SimplePeer.Instance): void; + _resume(): void; + start(): void; + _pause(): void; + pause(): void; + resume(): void; + cancel(): void; +} +export {}; diff --git a/dist/PeerFileSend.js b/dist/PeerFileSend.js new file mode 100644 index 0000000..358b249 --- /dev/null +++ b/dist/PeerFileSend.js @@ -0,0 +1,200 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +var ee_ts_1 = require("ee-ts"); +var readable_stream_1 = require("readable-stream"); +var read = require("filereader-stream"); +var Meta_1 = require("./Meta"); +var CHUNK_SIZE = Math.pow(2, 16); +/** + * Make a Uint8Array to send to peer + * @param header Type of data. See Meta.ts + * @param data + */ +function pMsg(header, data) { + if (data === void 0) { data = null; } + var resp; + if (data) { + resp = new Uint8Array(1 + data.length); + resp.set(data, 1); + } + else { + resp = new Uint8Array(1); + } + resp[0] = header; + return resp; +} +var SendStream = /** @class */ (function (_super) { + __extends(SendStream, _super); + function SendStream(fileSize, bytesSent) { + if (bytesSent === void 0) { bytesSent = 0; } + var _this = _super.call(this) || this; + _this.bytesSent = 0; + _this.fileSize = 0; // file size + _this.paused = false; + _this.fileSize = fileSize; + _this.bytesSent = bytesSent; + return _this; + } + SendStream.prototype._read = function () { + if (this.cb) + this.cb(null); + }; + /** + * File stream writes here + * @param chunk + * @param encoding + * @param cb + */ + SendStream.prototype._write = function (chunk, encoding, cb) { + if (this.paused) + return; + // Will return true if additional chunks of data may continue to be pushed + var availableForMore = this.push(pMsg(Meta_1.ControlHeaders.FILE_CHUNK, chunk)); + this.bytesSent += chunk.byteLength; + var percentage = parseFloat((100 * (this.bytesSent / this.fileSize)).toFixed(3)); + this.emit('progress', percentage, this.bytesSent); + if (availableForMore) { + this.cb = null; + cb(null); // Signal that we're ready for more data + } + else { + this.cb = cb; + } + }; + return SendStream; +}(readable_stream_1.Duplex)); +var PeerFileSend = /** @class */ (function (_super) { + __extends(PeerFileSend, _super); + /** + * @param peer Peer to send + * @param file File to send + * @param offset Bytes to start sending from, useful for file resume + */ + function PeerFileSend(peer, file, offset) { + if (offset === void 0) { offset = 0; } + var _this = _super.call(this) || this; + _this.paused = false; + _this.cancelled = false; + _this.receiverPaused = false; + // Bytes to start sending from + _this.offset = 0; + _this.peer = peer; + _this.file = file; + _this.offset = offset; + return _this; + } + /** + * Send a message to receiver + * @param header Type of message + * @param data Message + */ + PeerFileSend.prototype.sendPeer = function (header, data) { + if (data === void 0) { data = null; } + if (!this.peer.connected) + return; + this.peer.send(pMsg(header, data)); + }; + // Info about file is sent first + PeerFileSend.prototype.sendFileStartData = function () { + var meta = { + fileName: this.file.name, + fileSize: this.file.size, + fileType: this.file.type + }; + var metaString = JSON.stringify(meta); + var metaByteArray = new TextEncoder().encode(metaString); + this.sendPeer(Meta_1.ControlHeaders.FILE_START, metaByteArray); + }; + PeerFileSend.prototype.setPeer = function (peer) { + this.peer = peer; + }; + // Start sending file to receiver + PeerFileSend.prototype._resume = function () { + var _this = this; + if (this.receiverPaused) + return; + if (this.offset === 0) { + // Start + this.sendFileStartData(); + this.emit('progress', 0.0, 0); + } + // Chunk sending + var stream = read(this.file, { + offset: this.offset, + chunkSize: CHUNK_SIZE + }); + this.ss = new SendStream(this.file.size, this.offset); + this.ss.on('progress', function (percentage, bytes) { + _this.emit('progress', percentage, bytes); + }); + stream.pipe(this.ss).pipe(this.peer); + }; + PeerFileSend.prototype.start = function () { + var _this = this; + // Listen for cancel requests + this.peer.on('data', function (data) { + if (data[0] === Meta_1.ControlHeaders.FILE_END) { + _this.emit('progress', 100.0, _this.file.size); + _this.emit('done'); + } + else if (data[0] === Meta_1.ControlHeaders.TRANSFER_PAUSE) { + _this._pause(); + _this.receiverPaused = true; + _this.emit('paused'); + } + else if (data[0] === Meta_1.ControlHeaders.TRANSFER_RESUME) { + _this.receiverPaused = false; + if (!_this.paused) { + _this._resume(); + _this.emit('resumed'); + } + } + else if (data[0] === Meta_1.ControlHeaders.TRANSFER_CANCEL) { + _this.cancelled = true; + _this.peer.destroy(); + _this.emit('cancelled'); + } + }); + this._resume(); + }; + // Pause transfer and store the bytes sent till now for resuming later + PeerFileSend.prototype._pause = function () { + this.ss.paused = true; + this.offset = this.ss.bytesSent; + }; + // Stop sending data now & future sending + PeerFileSend.prototype.pause = function () { + this._pause(); + this.paused = true; + this.sendPeer(Meta_1.ControlHeaders.TRANSFER_PAUSE); + this.emit('pause'); + }; + // Allow data to be sent & start sending data + PeerFileSend.prototype.resume = function () { + this.paused = false; + this._resume(); + this.emit('resume'); + }; + PeerFileSend.prototype.cancel = function () { + this.cancelled = true; + this.ss.destroy(); + this.sendPeer(Meta_1.ControlHeaders.TRANSFER_CANCEL); + this.peer.destroy(); + this.emit('cancel'); + }; + return PeerFileSend; +}(ee_ts_1.EventEmitter)); +exports.default = PeerFileSend; diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..cbc840e --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,15 @@ +/*! + * Simple library to send files over WebRTC + * + * @author Subin Siby + * @license MPL-2.0 + */ +import * as Peer from 'simple-peer'; +import PeerFileSend from './PeerFileSend'; +import PeerFileReceive from './PeerFileReceive'; +export default class SimplePeerFiles { + private arrivals; + send(peer: Peer, fileID: string, file: File): Promise; + receive(peer: Peer, fileID: string): Promise; +} +export { PeerFileSend, PeerFileReceive }; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..42198fc --- /dev/null +++ b/dist/index.js @@ -0,0 +1,128 @@ +"use strict"; +/*! + * Simple library to send files over WebRTC + * + * @author Subin Siby + * @license MPL-2.0 + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.PeerFileReceive = exports.PeerFileSend = void 0; +var Peer = require("simple-peer"); +var PeerFileSend_1 = require("./PeerFileSend"); +exports.PeerFileSend = PeerFileSend_1.default; +var PeerFileReceive_1 = require("./PeerFileReceive"); +exports.PeerFileReceive = PeerFileReceive_1.default; +var SimplePeerFiles = /** @class */ (function () { + function SimplePeerFiles() { + this.arrivals = {}; + } + SimplePeerFiles.prototype.send = function (peer, fileID, file) { + return new Promise(function (resolve) { + var controlChannel = peer; + var startingByte = 0; + var fileChannel = new Peer({ + initiator: true + }); + fileChannel.on('signal', function (signal) { + controlChannel.send(JSON.stringify({ + fileID: fileID, + signal: signal + })); + }); + var controlDataHandler = function (data) { + try { + var dataJSON = JSON.parse(data); + if (dataJSON.signal && dataJSON.fileID && dataJSON.fileID === fileID) { + if (dataJSON.start) { + startingByte = dataJSON.start; + } + fileChannel.signal(dataJSON.signal); + } + } + catch (e) { } + }; + fileChannel.on('connect', function () { + var pfs = new PeerFileSend_1.default(fileChannel, file, startingByte); + var destroyed = false; + var destroy = function () { + if (destroyed) + return; + controlChannel.removeListener('data', controlDataHandler); + fileChannel.destroy(); + // garbage collect + controlDataHandler = null; + pfs = null; + destroyed = true; + }; + pfs.on('done', destroy); + pfs.on('cancel', destroy); + fileChannel.on('close', function () { + // cancel pfs if its available + pfs === null || pfs === void 0 ? void 0 : pfs.cancel(); + }); + resolve(pfs); + }); + controlChannel.on('data', controlDataHandler); + }); + }; + SimplePeerFiles.prototype.receive = function (peer, fileID) { + var _this = this; + return new Promise(function (resolve) { + var controlChannel = peer; + var fileChannel = new Peer({ + initiator: false + }); + fileChannel.on('signal', function (signal) { + // chunk to start sending from + var start = 0; + // File resume capability + if (fileID in _this.arrivals) { + start = _this.arrivals[fileID].bytesReceived; + } + controlChannel.send(JSON.stringify({ + fileID: fileID, + start: start, + signal: signal + })); + }); + var controlDataHandler = function (data) { + try { + var dataJSON = JSON.parse(data); + if (dataJSON.signal && dataJSON.fileID && dataJSON.fileID === fileID) { + fileChannel.signal(dataJSON.signal); + } + } + catch (e) { } + }; + fileChannel.on('connect', function () { + var pfs; + if (fileID in _this.arrivals) { + pfs = _this.arrivals[fileID]; + pfs.setPeer(fileChannel); + } + else { + pfs = new PeerFileReceive_1.default(fileChannel); + _this.arrivals[fileID] = pfs; + } + var destroyed = false; + var destroy = function () { + if (destroyed) + return; + controlChannel.removeListener('data', controlDataHandler); + fileChannel.destroy(); + delete _this.arrivals[fileID]; + // garbage collect + controlDataHandler = null; + pfs = null; + destroyed = true; + }; + pfs.on('done', destroy); + pfs.on('cancel', destroy); + resolve(pfs); + }); + controlChannel.on('data', controlDataHandler); + }); + }; + return SimplePeerFiles; +}()); +exports.default = SimplePeerFiles; diff --git a/src/index.ts b/src/index.ts index cf4a3c6..32c3509 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,7 +65,8 @@ export default class SimplePeerFiles { pfs.on('cancel', destroy) fileChannel.on('close', () => { - pfs.cancel() + // cancel pfs if its available + pfs?.cancel() }) resolve(pfs)