|
|
@@ -6,6 +6,7 @@ const Path = require('path');
|
|
|
const Url = require('url');
|
|
|
const Util = require('util');
|
|
|
|
|
|
+const Aws = require('aws-sdk');
|
|
|
const Mkdirp = require('mkdirp');
|
|
|
const Pati = require('pati');
|
|
|
const WriteFileAtomic = require('write-file-atomic');
|
|
|
@@ -24,28 +25,64 @@ class HlsUploader {
|
|
|
|
|
|
constructor(targetUri, options) {
|
|
|
|
|
|
- Assert.equal(Url.parse(targetUri).protocol, null);
|
|
|
+ const url = Url.parse(targetUri);
|
|
|
+ Assert.ok(url.protocol === null || url.protocol === 's3:');
|
|
|
|
|
|
this.targetUri = targetUri;
|
|
|
|
|
|
this.indexName = options.indexName || 'index.m3u8';
|
|
|
this.collect = !!options.collect;
|
|
|
+ this.cacheDuration = options.cacheDuration || 7 * 24 * 3600 * 1000;
|
|
|
|
|
|
// State
|
|
|
|
|
|
this.lastIndexString = '';
|
|
|
this.segmentBytes = 0;
|
|
|
|
|
|
- // TODO: make async?
|
|
|
- if (!Fs.existsSync(this.targetUri)) {
|
|
|
- Mkdirp.sync(this.targetUri);
|
|
|
+ if (url.protocol === 's3:') {
|
|
|
+ Assert.equal(options.collect, false, 'Collect not supported with s3:');
|
|
|
+
|
|
|
+ const params = {
|
|
|
+ params: {
|
|
|
+ Bucket: url.host,
|
|
|
+ ACL: 'public-read',
|
|
|
+ StorageClass: 'REDUCED_REDUNDANCY'
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ this.s3 = new Aws.S3(params);
|
|
|
+ this.baseKey = (url.pathname || '/').slice(1);
|
|
|
+ } else {
|
|
|
+ // TODO: make async?
|
|
|
+ if (!Fs.existsSync(this.targetUri)) {
|
|
|
+ Mkdirp.sync(this.targetUri);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- async pushSegment(stream, name) {
|
|
|
+ async pushSegment(stream, name, meta) {
|
|
|
|
|
|
- const target = this.prepareTargetStream(name);
|
|
|
+ const append = this.collect && this.segmentBytes !== 0;
|
|
|
+
|
|
|
+ if (this.s3) {
|
|
|
+ const params = {
|
|
|
+ Body: stream,
|
|
|
+ Key: Path.join(this.baseKey, name),
|
|
|
+ ContentType: meta.mime || 'video/MP2T',
|
|
|
+ CacheControl: `max-age=${Math.floor(this.cacheDuration / 1000)}, public`,
|
|
|
+ ContentLength: meta.size
|
|
|
+ };
|
|
|
+
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+
|
|
|
+ this.s3.upload(params, (err, data) => {
|
|
|
|
|
|
+ return err ? reject(err) : resolve(data);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ const target = Fs.createWriteStream(Path.join(this.targetUri, name), { flags: append ? 'a' : 'w' });
|
|
|
stream.pipe(target);
|
|
|
|
|
|
const dispatcher = new Pati.EventDispatcher(stream);
|
|
|
@@ -68,15 +105,28 @@ class HlsUploader {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- prepareTargetStream(name) {
|
|
|
+ async flushIndex(index) {
|
|
|
|
|
|
- const append = this.collect && this.segmentBytes !== 0;
|
|
|
- return Fs.createWriteStream(Path.join(this.targetUri, name), { flags: append ? 'a' : 'w' });
|
|
|
- }
|
|
|
+ const indexString = index.toString().trim();
|
|
|
|
|
|
- flushIndex(index) {
|
|
|
+ if (this.s3) {
|
|
|
+ const cacheTime = index.ended ? this.cacheDuration : index.target_duration * 1000 / 2;
|
|
|
|
|
|
- const indexString = index.toString().trim();
|
|
|
+ const params = {
|
|
|
+ Body: indexString,
|
|
|
+ Key: Path.join(this.baseKey, this.indexName),
|
|
|
+ ContentType: 'application/vnd.apple.mpegURL',
|
|
|
+ CacheControl: `max-age=${Math.floor(cacheTime / 1000)}, public`
|
|
|
+ };
|
|
|
+
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+
|
|
|
+ this.s3.putObject(params, (err, data) => {
|
|
|
+
|
|
|
+ return err ? reject(err) : resolve(data);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
let appendString;
|
|
|
if (this.lastIndexString && indexString.startsWith(this.lastIndexString)) {
|