From 8f086cac885e360a0c6a2944377d488c5712f70d Mon Sep 17 00:00:00 2001 From: Sam Patzer Date: Wed, 15 Jul 2020 11:59:33 -0700 Subject: [PATCH] Adding support to new AWS Service (#127) * IVS Support --- commands/video/setup-obs.js | 11 +- .../ivs-helpers/IVS-Channel.template | 29 ++ .../ivs-helpers/IVS-Custom.template | 70 ++++ .../LambdaFunctions/IVSShim/index.js | 156 +++++++ .../LambdaFunctions/IVSShim/package-lock.json | 383 ++++++++++++++++++ .../LambdaFunctions/IVSShim/package.json | 15 + .../ivs-workflow-template.yaml.ejs | 49 +++ .../service-walkthroughs/ivs-push.js | 39 ++ .../awscloudformation/utils/livestream-obs.js | 30 +- .../awscloudformation/utils/video-getinfo.js | 31 +- .../awscloudformation/utils/video-staging.js | 21 +- provider-utils/ivs-questions.json | 17 + provider-utils/supported-services.json | 40 +- 13 files changed, 850 insertions(+), 41 deletions(-) create mode 100644 provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/IVS-Channel.template create mode 100644 provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/IVS-Custom.template create mode 100644 provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/LambdaFunctions/IVSShim/index.js create mode 100644 provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/LambdaFunctions/IVSShim/package-lock.json create mode 100644 provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/LambdaFunctions/IVSShim/package.json create mode 100644 provider-utils/awscloudformation/cloudformation-templates/ivs-workflow-template.yaml.ejs create mode 100644 provider-utils/awscloudformation/service-walkthroughs/ivs-push.js create mode 100644 provider-utils/ivs-questions.json diff --git a/commands/video/setup-obs.js b/commands/video/setup-obs.js index 62ee65d4..a0ef6fae 100644 --- a/commands/video/setup-obs.js +++ b/commands/video/setup-obs.js @@ -14,13 +14,20 @@ module.exports = { return; } + const filteredProjects = Object.keys(amplifyMeta[category]).filter(project => ( + amplifyMeta[category][project].serviceType === 'livestream' || amplifyMeta[category][project].serviceType === 'ivs')); + if (filteredProjects.length === 0) { + context.print.error('You have no livestreaming projects.'); + return; + } + const chooseProject = [ { type: 'list', name: 'resourceName', message: 'Choose what project you want to set up OBS for?', - choices: Object.keys(amplifyMeta[category]), - default: Object.keys(amplifyMeta[category])[0], + choices: filteredProjects, + default: filteredProjects[0], }, ]; const props = await inquirer.prompt(chooseProject); diff --git a/provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/IVS-Channel.template b/provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/IVS-Channel.template new file mode 100644 index 00000000..cba7184c --- /dev/null +++ b/provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/IVS-Channel.template @@ -0,0 +1,29 @@ +Description: S3 Workflow + +Parameters: + pProjectName: + Type: String + Description: ProjectName + AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" + Default: DefaultName + pFunctionArn: + Type: String + Description: Name of function + Default: arn-default +Outputs: + oVideoChannelArn: + Value: !GetAtt rIVSChannel.arn + oVideoOutput: + Value: !GetAtt rIVSChannel.playbackUrl + oVideoInputURL: + Value: !GetAtt rIVSChannel.ingestURL + oVideoInputKey: + Value: !GetAtt rIVSChannel.streamKeyValue + +Resources: + rIVSChannel: + Type: "Custom::StarfruitChannel" + Properties: + ServiceToken: !Ref pFunctionArn + name: !Ref pProjectName + API: VideoChannel \ No newline at end of file diff --git a/provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/IVS-Custom.template b/provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/IVS-Custom.template new file mode 100644 index 00000000..96502813 --- /dev/null +++ b/provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/IVS-Custom.template @@ -0,0 +1,70 @@ +Description: IVS Shim Template + +Parameters: + pS3: + Type: String + Description: Store template and lambda package + AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" + Default: amazonbooth + pFunctionName: + Type: String + Description: Name of function + Default: default + pFunctionHash: + Type: String + Description: FunctionHash + Default: default + +Outputs: + oIVSLambda: + Value: !GetAtt rIVSChannelLambda.Arn + Description: Arn for Lambda function + +Resources: + rIVSChannelLambda: + Type: AWS::Lambda::Function + Properties: + FunctionName: !Ref pFunctionName + Description: A shim for IVS support. Can do anything that the API can do + Handler: index.handler + Role: !GetAtt rIVSRole.Arn + Runtime: nodejs12.x + Timeout: 30 + Code: + S3Bucket: !Ref pS3 + S3Key: !Sub + - ivs-helpers/IVSShim-${hash}.zip + - { hash: !Ref pFunctionHash } + + rIVSRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - + Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - + PolicyName: !Sub "${AWS::StackName}-internal-trigger-role" + PolicyDocument: + Statement: + - + Effect: Allow + Action: + - ivs:* + Resource: "*" + - + Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:DescribeLogStreams + - logs:PutLogEvents + Resource: + - arn:aws:logs:*:*:* \ No newline at end of file diff --git a/provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/LambdaFunctions/IVSShim/index.js b/provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/LambdaFunctions/IVSShim/index.js new file mode 100644 index 00000000..2cf57db2 --- /dev/null +++ b/provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/LambdaFunctions/IVSShim/index.js @@ -0,0 +1,156 @@ +/* eslint-disable strict */ +/* eslint-disable global-require */ +/* eslint-disable */ +const crypto = require('crypto'); +const rp = require('request-promise-native'); + +const SigV4Utils = { + sign(key, msg) { + return crypto.createHmac('sha256', key).update(msg).digest().toString('hex'); + }, + sha256(msg) { + return crypto.createHash('sha256').update(msg, 'utf8').digest().toString('hex'); + }, + getSignatureKey(key, dateStamp, regionName, serviceName) { + const kDate = crypto.createHmac('sha256', `AWS4${key}`).update(dateStamp).digest(); + const kRegion = crypto.createHmac('sha256', kDate).update(regionName).digest(); + const kService = crypto.createHmac('sha256', kRegion).update(serviceName).digest(); + const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest(); + return kSigning; + }, +}; + +exports.handler = async (event, context) => { + const config = event.ResourceProperties; + let responseData = {}; + switch (event.RequestType) { + case 'Create': + responseData = await createIVS(config); + break; + case 'Delete': + responseData = await deleteIVS(event, config); + break; + case 'Update': + responseData = await updateIVS(event, config); + break; + default: + console.log(`${event.RequestType} is not supported. No changes are applied`); + } + const response = await sendResponse(event, context, 'SUCCESS', responseData); + console.log('CFN STATUS:: ', response, responseData); +}; + +async function createIVS(config) { + if (config.API === 'VideoChannel') { + const flattenResults = {}; + const results = await signAndRequest('POST', '/CreateChannel', config); + flattenResults.arn = results.channel.arn; + flattenResults.playbackUrl = results.channel.playbackUrl; + flattenResults.streamKeyValue = results.streamKey.value; + flattenResults.ingestURL = results.channel.ingestEndpoint; + return flattenResults; + } + return { error: "API doesn't exists" }; +} + +async function updateIVS(config) { + console.log(config); + return {}; +} + +async function deleteIVS(event, config) { + if (config.API === 'VideoChannel') { + const deleteRequest = { + arn: event.PhysicalResourceId, + }; + const results = await signAndRequest('POST', '/DeleteChannel', deleteRequest); + if (results) { + results.arn = event.PhysicalResourceId; + results.message = 'Delete was successful'; + return results; + } + deleteRequest.message = 'Delete was successful'; + return deleteRequest; + } +} + +async function signAndRequest(method, uriRaw, config) { + const service = 'ivs'; + const region = process.env.AWS_REGION; + const accessKey = process.env.AWS_ACCESS_KEY_ID; + const secretKey = process.env.AWS_SECRET_ACCESS_KEY; + const token = encodeURIComponent(process.env.AWS_SESSION_TOKEN); + const algorithm = 'AWS4-HMAC-SHA256'; + const host = 'ivs.us-west-2.amazonaws.com'; + const canonicalUri = uriRaw; + + const now = new Date(); + const amzdate = `${now.toISOString().replace(/[-:]/g, '').split('.')[0]}Z`; + const dateStamp = amzdate.split('T')[0]; + const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`; + let canonicalQuerystring = 'X-Amz-Algorithm=AWS4-HMAC-SHA256'; + canonicalQuerystring += `&X-Amz-Credential=${encodeURIComponent(`${accessKey}/${credentialScope}`)}`; + canonicalQuerystring += `&X-Amz-Date=${amzdate}`; + canonicalQuerystring += '&X-Amz-Expires=86400'; + canonicalQuerystring += `&X-Amz-Security-Token=${token}`; + canonicalQuerystring += '&X-Amz-SignedHeaders=host'; + delete config.ServiceToken; + delete config.API; + + const canonicalHeaders = `host:${host}\n`; + const payloadHash = SigV4Utils.sha256(JSON.stringify(config)); + const canonicalRequest = `${method}\n${canonicalUri}\n${canonicalQuerystring}\n${canonicalHeaders}\nhost\n${payloadHash}`; + + const stringToSign = `${algorithm}\n${amzdate}\n${credentialScope}\n${SigV4Utils.sha256(canonicalRequest)}`; + const signingKey = SigV4Utils.getSignatureKey(secretKey, dateStamp, region, service); + const signature = SigV4Utils.sign(signingKey, stringToSign); + + canonicalQuerystring += `&X-Amz-Signature=${signature}`; + const requestURL = `${host}${canonicalUri}?${canonicalQuerystring}`; + const options = { + method, + uri: `https://${requestURL}`, + body: config, + json: true, + }; + const urlReturn = await rp(options); + console.log(urlReturn); + + return urlReturn; +} + +async function sendResponse(event, context, responseStatus, responseData) { + const responseBodyDictionary = { + Status: responseStatus, + Reason: `See the details in CloudWatch Log Stream: ${context.logStreamName}`, + StackId: event.StackId, + RequestId: event.RequestId, + LogicalResourceId: event.LogicalResourceId, + Data: responseData, + }; + if (responseData !== undefined) { + responseBodyDictionary.PhysicalResourceId = responseData.arn; + responseBodyDictionary.Data = responseData; + } + + const responseBody = JSON.stringify(responseBodyDictionary); + + console.log('RESPONSE BODY:\n', responseBody); + + const options = { + method: 'PUT', + uri: event.ResponseURL, + body: responseBody, + headers: { + 'content-type': '', + 'content-length': responseBody.length, + }, + }; + const urlReturn = await rp(options); + console.log('urlReturn: ', urlReturn); + + console.log('options: ', options); + + console.log('SENDING RESPONSE...\n'); + return urlReturn; +} diff --git a/provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/LambdaFunctions/IVSShim/package-lock.json b/provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/LambdaFunctions/IVSShim/package-lock.json new file mode 100644 index 00000000..606eadbe --- /dev/null +++ b/provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/LambdaFunctions/IVSShim/package-lock.json @@ -0,0 +1,383 @@ +{ + "name": "starfruitchannellambda", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ajv": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", + "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", + "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "mime-db": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" + }, + "mime-types": { + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "requires": { + "mime-db": "1.43.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "psl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", + "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + } + } + }, + "request-promise-core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", + "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "requires": { + "lodash": "^4.17.15" + } + }, + "request-promise-native": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", + "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", + "requires": { + "request-promise-core": "1.1.3", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + } + } +} diff --git a/provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/LambdaFunctions/IVSShim/package.json b/provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/LambdaFunctions/IVSShim/package.json new file mode 100644 index 00000000..3a8de2f5 --- /dev/null +++ b/provider-utils/awscloudformation/cloudformation-templates/ivs-helpers/LambdaFunctions/IVSShim/package.json @@ -0,0 +1,15 @@ +{ + "name": "starfruitchannellambda", + "version": "1.0.0", + "description": "Create and delete Starfruit Channels", + "main": "index.js", + "scripts": { + "test": "node index.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "request": "^2.88.0", + "request-promise-native": "^1.0.8" + } +} diff --git a/provider-utils/awscloudformation/cloudformation-templates/ivs-workflow-template.yaml.ejs b/provider-utils/awscloudformation/cloudformation-templates/ivs-workflow-template.yaml.ejs new file mode 100644 index 00000000..cfde5aed --- /dev/null +++ b/provider-utils/awscloudformation/cloudformation-templates/ivs-workflow-template.yaml.ejs @@ -0,0 +1,49 @@ +Description: <%= props.shared.resourceName %> + +Parameters: + env: + Type: String + Description: The environment name. e.g. Dev, Test, or Production. + Default: NONE + pS3: + Type: String + Description: Store template and lambda package + AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" + Default: "<%= props.shared.bucket %>" + pSourceFolder: + Type: String + Description: Store template and lambda package + AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" + Default: ivs-helpers + pProjectName: + Type: String + Description: ProjectName + AllowedPattern: "[a-zA-Z][a-zA-Z0-9-_]*" + Default: <%= props.shared.resourceName %> + +Resources: + rIVSChannel: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: !Sub "https://s3.amazonaws.com/${pS3}/${pSourceFolder}/IVS-Channel.template" + Parameters: + pProjectName: !Ref pProjectName + pFunctionArn: !GetAtt rIVSShimLambda.Outputs.oIVSLambda + rIVSShimLambda: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: !Sub "https://s3.amazonaws.com/${pS3}/${pSourceFolder}/IVS-Custom.template" + Parameters: + pS3: !Ref pS3 + pFunctionName: !Ref pProjectName + pFunctionHash: <%= props.hashes.IVSShim %> + +Outputs: + oVideoOutput: + Value: !GetAtt rIVSChannel.Outputs.oVideoOutput + oVideoInputURL: + Value: !GetAtt rIVSChannel.Outputs.oVideoInputURL + oVideoInputKey: + Value: !GetAtt rIVSChannel.Outputs.oVideoInputKey + oVideoChannelArn: + Value: !GetAtt rIVSChannel.Outputs.oVideoChannelArn \ No newline at end of file diff --git a/provider-utils/awscloudformation/service-walkthroughs/ivs-push.js b/provider-utils/awscloudformation/service-walkthroughs/ivs-push.js new file mode 100644 index 00000000..bb65dc75 --- /dev/null +++ b/provider-utils/awscloudformation/service-walkthroughs/ivs-push.js @@ -0,0 +1,39 @@ +const inquirer = require('inquirer'); +const question = require('../../ivs-questions.json'); + +module.exports = { + serviceQuestions, +}; + +async function serviceQuestions(context, options, defaultValuesFilename, resourceName) { + const { amplify } = context; + const projectMeta = context.amplify.getProjectMeta(); + // const projectDetails = context.amplify.getProjectDetails(); + // const defaultLocation = + // path.resolve(`${__dirname}/../default-values/${defaultValuesFilename}`); + // const defaults = JSON.parse(fs.readFileSync(`${defaultLocation}`)); + // const targetDir = amplify.pathManager.getBackendDirPath(); + const props = {}; + let nameDict = {}; + + const { inputs } = question.video; + const nameProject = [ + { + type: inputs[0].type, + name: inputs[0].key, + message: inputs[0].question, + validate: amplify.inputValidation(inputs[0]), + default: 'mylivestream', + }]; + + if (resourceName) { + nameDict.resourceName = resourceName; + props.shared = nameDict; + } else { + nameDict = await inquirer.prompt(nameProject); + props.shared = nameDict; + } + props.shared.bucket = projectMeta.providers.awscloudformation.DeploymentBucketName; + + return props; +} diff --git a/provider-utils/awscloudformation/utils/livestream-obs.js b/provider-utils/awscloudformation/utils/livestream-obs.js index 31ff0e08..b1799f11 100644 --- a/provider-utils/awscloudformation/utils/livestream-obs.js +++ b/provider-utils/awscloudformation/utils/livestream-obs.js @@ -10,15 +10,13 @@ async function setupOBS(context, resourceName) { const { amplify } = context; const amplifyMeta = amplify.getProjectMeta(); if ('output' in amplifyMeta.video[resourceName]) { - if ('oMediaLivePrimaryIngestUrl' in amplifyMeta.video[resourceName].output) { - await createConfig(context, amplifyMeta.video[resourceName].output, resourceName); - } + await createConfig(context, amplifyMeta.video[resourceName], resourceName); } else { context.print.warning(chalk`{bold You have not pushed ${resourceName} to the cloud yet.}`); } } -async function createConfig(context, output, projectName) { +async function createConfig(context, projectConfig, projectName) { // check for obs installation! let profileDir = ''; if (process.platform === 'darwin') { @@ -43,24 +41,40 @@ async function createConfig(context, output, projectName) { } generateINI(projectName, profileDir); - generateService(profileDir, output.oMediaLivePrimaryIngestUrl); + if (projectConfig.serviceType === 'livestream') { + generateServiceLive(profileDir, projectConfig.output.oMediaLivePrimaryIngestUrl); + } else if (projectConfig.serviceType === 'ivs') { + generateServiceIVS(profileDir, projectConfig.output); + } context.print.success('\nConfiguration complete.'); context.print.blue(chalk`Open OBS and select {bold ${projectName}} profile to use the generated profile for OBS`); } -async function generateINI(projectName, directory) { +function generateINI(projectName, directory) { const iniBasic = ini.parse(fs.readFileSync(`${__dirname}/../obs-templates/basic.ini`, 'utf-8')); iniBasic.General.Name = projectName; fs.writeFileSync(`${directory}basic.ini`, ini.stringify(iniBasic)); } -async function generateService(directory, primaryURL) { +function generateServiceIVS(directory, projectOutput) { + const setup = { + settings: { + key: projectOutput.oVideoInputKey, + server: projectOutput.oVideoInputURL, + }, + type: 'rtmp_custom', + }; + const json = JSON.stringify(setup); + fs.writeFileSync(`${directory}service.json`, json); +} + +function generateServiceLive(directory, primaryURL) { const primaryKey = primaryURL.split('/'); const setup = { settings: { key: primaryKey[3], - server: primaryURL, + server: `rtmps://${primaryURL}`, }, type: 'rtmp_custom', }; diff --git a/provider-utils/awscloudformation/utils/video-getinfo.js b/provider-utils/awscloudformation/utils/video-getinfo.js index 04c540e5..b4bca3fe 100644 --- a/provider-utils/awscloudformation/utils/video-getinfo.js +++ b/provider-utils/awscloudformation/utils/video-getinfo.js @@ -11,6 +11,13 @@ async function getInfoVideoAll(context) { if ('video' in amplifyMeta && Object.keys(amplifyMeta.video).length !== 0) { Object.values(amplifyMeta.video).forEach((project) => { if ('output' in project) { + if (project.serviceType === 'video-on-demand') { + prettifyOutputVod(context, project.output); + } else if (project.serviceType === 'livestream') { + prettifyOutputLive(context, project.output); + } else if (project.serviceType === 'ivs') { + prettifyOutputIVS(context, project.output); + } if ('oMediaLivePrimaryIngestUrl' in project.output) { prettifyOutputLive(context, project.output); } else if ('oVODInputS3' in project.output) { @@ -84,10 +91,12 @@ async function generateAWSExportsVideo(context) { async function getVideoInfo(context, resourceName) { const amplifyMeta = context.amplify.getProjectMeta(); if ('output' in amplifyMeta.video[resourceName]) { - if ('oMediaLivePrimaryIngestUrl' in amplifyMeta.video[resourceName].output) { - await prettifyOutputLive(context, amplifyMeta.video[resourceName].output); - } else { + if (amplifyMeta.video[resourceName].serviceType === 'video-on-demand') { await prettifyOutputVod(context, amplifyMeta.video[resourceName].output); + } else if (amplifyMeta.video[resourceName].serviceType === 'livestream') { + await prettifyOutputLive(context, amplifyMeta.video[resourceName].output); + } else if (amplifyMeta.video[resourceName].serviceType === 'ivs') { + await prettifyOutputIVS(context, amplifyMeta.video[resourceName].output); } await generateAWSExportsVideo(context); } else { @@ -95,7 +104,18 @@ async function getVideoInfo(context, resourceName) { } } +async function prettifyOutputIVS(context, output) { + context.print.info(chalk.bold('\nInteractive Video Service:')); + context.print.blue('\nInput url:'); + context.print.blue(chalk`{underline rtmps://${output.oVideoInputURL}}\n`); + context.print.blue('\nStream Keys:'); + context.print.blue(`${output.oVideoInputKey}\n`); + context.print.blue('\nOutput url:'); + context.print.blue(chalk`{underline ${output.oVideoOutput}}\n`); +} + async function prettifyOutputLive(context, output) { + context.print.info(chalk.bold('\nLivestream Info:')); context.print.info(chalk.bold('\nMediaLive')); context.print.blue(chalk`MediaLive Primary Ingest Url: {underline ${output.oMediaLivePrimaryIngestUrl}}`); const primaryKey = output.oMediaLivePrimaryIngestUrl.split('/'); @@ -128,11 +148,12 @@ async function prettifyOutputLive(context, output) { } async function prettifyOutputVod(context, output) { - context.print.blue('Input Storage bucket:'); + context.print.info(chalk.bold('\nVideo on Demand:')); + context.print.blue('\nInput Storage bucket:'); context.print.blue(`${output.oVODInputS3}\n`); if (output.oVodOutputUrl) { context.print.blue('Output URL for content:'); - context.print.blue(`${output.oVodOutputUrl}\n`); + context.print.blue(chalk`{underline https://${output.oVodOutputUrl}\n}`); } else { context.print.blue('Output Storage bucket:'); context.print.blue(`${output.oVODOutputS3}\n`); diff --git a/provider-utils/awscloudformation/utils/video-staging.js b/provider-utils/awscloudformation/utils/video-staging.js index 3921f8ad..1a3c639f 100644 --- a/provider-utils/awscloudformation/utils/video-staging.js +++ b/provider-utils/awscloudformation/utils/video-staging.js @@ -3,7 +3,6 @@ const childProcess = require('child_process'); const archiver = require('archiver'); const path = require('path'); const mime = require('mime-types'); -const chalk = require('chalk'); const ora = require('ora'); const ejs = require('ejs'); const YAML = require('yaml'); @@ -335,9 +334,9 @@ async function handleNodeInstall(packageDest) { stdio: 'pipe', encoding: 'utf-8', }); - if (childProcessResult.status !== 0) { - throw new Error(childProcessResult.output.join()); - } + childProcessResult.on('error', (error) => { + console.log(error); + }); return childProcessResult; } @@ -411,6 +410,9 @@ async function zipLambdaFunctionsAndPush(context, lambdaName, lambdaDir, zipDir, context.print.error(err); } }); + archive.on('end', async () => { + await uploadFile(context, s3Client, targetBucket, zipDir, newFilePath, stackFolder, hashName); + }); archive.on('error', (err) => { context.print.error(err); throw err; @@ -418,7 +420,6 @@ async function zipLambdaFunctionsAndPush(context, lambdaName, lambdaDir, zipDir, archive.pipe(output); archive.directory(lambdaDir, false); await archive.finalize(); - await uploadFile(context, s3Client, targetBucket, zipDir, newFilePath, stackFolder, hashName); } async function uploadFile(context, s3Client, hostingBucketName, distributionDirPath, filePath, @@ -436,11 +437,11 @@ async function uploadFile(context, s3Client, hostingBucketName, distributionDirP Body: fileStream, ContentType: contentType || 'text/plain', }; - s3Client.upload(uploadParams, (err) => { - if (err) { - context.print.error(chalk.bold('Failed uploading object to S3. Check your connection and try to running amplify push')); - } - }); + try { + await s3Client.upload(uploadParams).promise(); + } catch (error) { + context.print.error(`Failed pushing to S3 with error: ${error}`); + } } module.exports = { diff --git a/provider-utils/ivs-questions.json b/provider-utils/ivs-questions.json new file mode 100644 index 00000000..0af7fc86 --- /dev/null +++ b/provider-utils/ivs-questions.json @@ -0,0 +1,17 @@ +{ + "video": { + "inputs": [ + { + "key": "resourceName", + "question": "Provide a friendly name for your resource to be used as a label for this category in the project:", + "validation": { + "operator": "regex", + "value": "^[a-zA-Z0-9\\-]+$", + "onErrorMsg": "Resource name should be alphanumeric" + }, + "required": true + } + ], + "provider": "awscloudformation" + } +} diff --git a/provider-utils/supported-services.json b/provider-utils/supported-services.json index 1cd5a9ae..705aed0e 100644 --- a/provider-utils/supported-services.json +++ b/provider-utils/supported-services.json @@ -1,18 +1,26 @@ { - "livestream":{ - "alias":"Livestream", - "serviceWalkthroughFilename": "livestream-push.js", - "cfnFilename": "livestream-workflow-template.json.ejs", - "stackFolder":"livestream-helpers", - "defaultValuesFilename": "livestream-defaults.json", - "provider": "awscloudformation" - }, - "video-on-demand":{ - "alias":"Video On Demand", - "serviceWalkthroughFilename": "vod-push.js", - "cfnFilename": "vod-workflow-template.yaml.ejs", - "stackFolder":"vod-helpers", - "defaultValuesFilename": "vod-defaults.json", - "provider": "awscloudformation" - } + "livestream":{ + "alias":"Elemental Livestream", + "serviceWalkthroughFilename":"livestream-push.js", + "cfnFilename":"livestream-workflow-template.json.ejs", + "stackFolder":"livestream-helpers", + "defaultValuesFilename":"livestream-defaults.json", + "provider":"awscloudformation" + }, + "video-on-demand":{ + "alias":"Video On Demand", + "serviceWalkthroughFilename":"vod-push.js", + "cfnFilename":"vod-workflow-template.yaml.ejs", + "stackFolder":"vod-helpers", + "defaultValuesFilename":"vod-defaults.json", + "provider":"awscloudformation" + }, + "ivs":{ + "alias":"Interactive LiveStream (Beta)", + "serviceWalkthroughFilename":"ivs-push.js", + "cfnFilename":"ivs-workflow-template.yaml.ejs", + "stackFolder":"ivs-helpers", + "defaultValuesFilename":"ivs-defaults.json", + "provider":"awscloudformation" + } } \ No newline at end of file