├── .gitignore ├── cdk ├── .npmignore ├── jest.config.js ├── .gitignore ├── Makefile ├── bin │ └── cdk.ts ├── README.md ├── repack.js ├── package.json ├── tsconfig.json ├── lib │ ├── ui-stack.ts │ └── backend-stack.ts └── cdk.json ├── amazon-ivs-clip-demo.png ├── clips ├── Makefile ├── go.mod ├── remux │ └── remux.go ├── hls │ └── hls.go ├── clips.go └── go.sum ├── CODE_OF_CONDUCT.md ├── LICENSE ├── .github └── workflows │ └── update-ivs-player-sdk.yml ├── README.md ├── CONTRIBUTING.md ├── web-ui ├── index.html ├── clip.js └── style.css └── THIRD-PARTY-LICENSES.txt /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | tmp/ 3 | tmp/* 4 | 5 | .DS_Store 6 | 7 | /bin 8 | 9 | web-ui/config.json -------------------------------------------------------------------------------- /cdk/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /amazon-ivs-clip-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ivs-clip-web-demo/HEAD/amazon-ivs-clip-demo.png -------------------------------------------------------------------------------- /cdk/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/test'], 3 | testMatch: ['**/*.test.ts'], 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /clips/Makefile: -------------------------------------------------------------------------------- 1 | clean: 2 | rm -rf ../bin 3 | 4 | build: clean 5 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o ../bin/clips clips.go 6 | rm -f ../bin/clips.zip && zip -r ../bin/clips.zip ../bin/clips -------------------------------------------------------------------------------- /cdk/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | # Parcel default cache directory 11 | .parcel-cache 12 | 13 | !repack.js 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /clips/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws-samples/amazon-ivs-clip-web-demo/clips 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/aws/aws-lambda-go v1.20.0 // indirect 7 | github.com/aws/aws-sdk-go v1.35.30 // indirect 8 | github.com/google/go-licenses v0.0.0-20201026145851-73411c8fa237 // indirect 9 | github.com/google/uuid v1.1.2 // indirect 10 | github.com/grafov/m3u8 v0.11.1 11 | ) 12 | -------------------------------------------------------------------------------- /cdk/Makefile: -------------------------------------------------------------------------------- 1 | build: app cdksynth 2 | 3 | buildcdk: 4 | npm install && npm run build 5 | 6 | cdksynth: buildcdk 7 | npx cdk -v synth ClipsBackend 8 | npx cdk -v synth ClipsUI 9 | 10 | deploy: build 11 | npx cdk deploy -v --outputs-file tempvars.json ClipsBackend 12 | node repack.js 13 | rm tempvars.json 14 | npx cdk deploy -v ClipsUI 15 | 16 | app: 17 | cd ../clips && make build 18 | 19 | bootstrap: app 20 | npx cdk bootstrap 21 | -------------------------------------------------------------------------------- /cdk/bin/cdk.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { BackendStack } from '../lib/backend-stack'; 5 | import { UIStack } from '../lib/ui-stack'; 6 | 7 | 8 | const app = new cdk.App(); 9 | let backend = new BackendStack(app, 'ClipsBackend', { 10 | cdkProps: {}, 11 | }); 12 | 13 | let ui = new UIStack(app, 'ClipsUI', { 14 | cdkProps: {}, 15 | bucket: backend.bucket, 16 | cfDistro: backend.cfDistro, 17 | channel: backend.channel, 18 | streamkey: backend.streamkey, 19 | }); 20 | -------------------------------------------------------------------------------- /cdk/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your CDK TypeScript project! 2 | 3 | This is a blank project for TypeScript development with CDK. 4 | 5 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 6 | 7 | ## Useful commands 8 | 9 | * `npm run build` compile typescript to js 10 | * `npm run watch` watch for changes and compile 11 | * `npm run test` perform the jest unit tests 12 | * `cdk deploy` deploy this stack to your default AWS account/region 13 | * `cdk diff` compare deployed stack with current state 14 | * `cdk synth` emits the synthesized CloudFormation template 15 | -------------------------------------------------------------------------------- /cdk/repack.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require('fs'); 2 | 3 | const tempvars = readFileSync('tempvars.json'); 4 | 5 | const tempvars2= (JSON.parse(tempvars)); 6 | 7 | const publicConfig = { 8 | api: tempvars2.ClipsBackend.api, 9 | playbackURL: tempvars2.ClipsBackend.playbackUrl 10 | } 11 | 12 | const { writeFileSync } = require('fs'); 13 | 14 | const path = '../web-ui/config.json'; 15 | 16 | try { 17 | writeFileSync(path, JSON.stringify(publicConfig, null, 2), 'utf8'); 18 | console.log('Data successfully saved to disk'); 19 | } catch (error) { 20 | console.log('An error has occurred ', error); 21 | } 22 | -------------------------------------------------------------------------------- /cdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk", 3 | "version": "0.1.0", 4 | "bin": { 5 | "cdk": "bin/cdk.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^29.5.11", 15 | "@types/node": "20.10.4", 16 | "aws-cdk": "2.115.0", 17 | "jest": "^29.7.0", 18 | "ts-jest": "^29.1.1", 19 | "ts-node": "^10.9.2", 20 | "typescript": "~5.3.3" 21 | }, 22 | "dependencies": { 23 | "aws-cdk-lib": "2.115.0", 24 | "constructs": "^10.0.0", 25 | "jsonfile": "^6.1.0", 26 | "source-map-support": "^0.5.21" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /clips/remux/remux.go: -------------------------------------------------------------------------------- 1 | package remux 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "log" 7 | "os/exec" 8 | "syscall" 9 | 10 | "github.com/aws-samples/amazon-ivs-clip-web-demo/clips/hls" 11 | ) 12 | 13 | func Remux(segments []hls.Segment, path string) error { 14 | 15 | var buffer []byte 16 | for _, b := range segments { 17 | buffer = append(buffer, b.Data...) 18 | } 19 | 20 | cmd := exec.Command("/opt/bin/ffmpeg", "-f", "mpegts", "-i", "pipe:", "-c", "copy", "-f", "mp4", path) 21 | stdin, err := cmd.StdinPipe() 22 | if err != nil { 23 | return err 24 | } 25 | stderr, err := cmd.StderrPipe() 26 | if err != nil { 27 | return err 28 | } 29 | 30 | go func() { 31 | defer stdin.Close() 32 | stdin.Write(buffer) 33 | }() 34 | 35 | cmd.Start() 36 | 37 | slurp, err := ioutil.ReadAll(stderr) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | if err := cmd.Wait(); err != nil { 43 | 44 | if exiterr, ok := err.(*exec.ExitError); ok { 45 | 46 | if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 47 | ex := status.ExitStatus() 48 | if ex == -1 || ex == 255 { 49 | } else { 50 | return errors.New(string(slurp)) 51 | } 52 | } 53 | } 54 | 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /cdk/lib/ui-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import * as s3 from 'aws-cdk-lib/aws-s3'; 3 | import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; 4 | import * as s3deployment from 'aws-cdk-lib/aws-s3-deployment'; 5 | import * as ivs from 'aws-cdk-lib/aws-ivs'; 6 | import { Construct } from 'constructs'; 7 | 8 | export interface StackProps { 9 | cdkProps: cdk.StackProps; 10 | bucket: s3.Bucket; 11 | cfDistro: cloudfront.CloudFrontWebDistribution; 12 | channel: ivs.CfnChannel; 13 | streamkey: ivs.CfnStreamKey; 14 | } 15 | 16 | export class UIStack extends cdk.Stack { 17 | constructor(scope: Construct, id: string, props: StackProps) { 18 | super(scope, id, props.cdkProps); 19 | 20 | // Upload the frontend to S3 21 | new s3deployment.BucketDeployment(this, 'DeployFiles', { 22 | sources: [s3deployment.Source.asset('../web-ui')], 23 | destinationBucket: props.bucket, 24 | prune: false, 25 | distribution: props.cfDistro, 26 | distributionPaths: ['/index.html', '/clip.js', '/config.json', '/style.css'] 27 | }); 28 | 29 | 30 | new cdk.CfnOutput(this, 'url', { value: 'https://' + props.cfDistro.distributionDomainName }); 31 | new cdk.CfnOutput(this, 'ingestserver', { value: 'rtmps://' + props.channel.attrIngestEndpoint + ':443/app/'}); 32 | new cdk.CfnOutput(this, 'streamkey', { value: props.streamkey.attrValue }); 33 | 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /.github/workflows/update-ivs-player-sdk.yml: -------------------------------------------------------------------------------- 1 | name: Update Amazon IVS Player SDK 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' # Run daily at midnight UTC 6 | workflow_dispatch: # Allow manual triggering 7 | 8 | jobs: 9 | update-ivs-player: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | pull-requests: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Check for updates 18 | id: check-updates 19 | run: | 20 | # Extract current version from web-ui/index.html 21 | CURRENT_VERSION=$(grep -oP 'player\.live-video\.net/\K[0-9]+\.[0-9]+\.[0-9]+' web-ui/index.html) 22 | echo "Current version: $CURRENT_VERSION" 23 | 24 | # Get latest version from npm 25 | LATEST_VERSION=$(npm view amazon-ivs-player version) 26 | echo "Latest version: $LATEST_VERSION" 27 | 28 | # Compare versions 29 | if [ "$CURRENT_VERSION" != "$LATEST_VERSION" ]; then 30 | echo "Update available: $CURRENT_VERSION -> $LATEST_VERSION" 31 | echo "has_update=true" >> $GITHUB_OUTPUT 32 | echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT 33 | echo "new_version=$LATEST_VERSION" >> $GITHUB_OUTPUT 34 | 35 | # Update the script src in web-ui/index.html 36 | sed -i "s|player\.live-video\.net/$CURRENT_VERSION|player.live-video.net/$LATEST_VERSION|g" web-ui/index.html 37 | else 38 | echo "No update available" 39 | echo "has_update=false" >> $GITHUB_OUTPUT 40 | fi 41 | 42 | - name: Create Pull Request 43 | if: steps.check-updates.outputs.has_update == 'true' 44 | uses: peter-evans/create-pull-request@v6 45 | with: 46 | commit-message: 'chore: update Amazon IVS Player SDK to ${{ steps.check-updates.outputs.new_version }}' 47 | title: 'chore: update Amazon IVS Player SDK to ${{ steps.check-updates.outputs.new_version }}' 48 | body: | 49 | This PR updates the Amazon IVS Player SDK from ${{ steps.check-updates.outputs.current_version }} to ${{ steps.check-updates.outputs.new_version }}. 50 | 51 | The script source in `web-ui/index.html` has been updated to use the latest version from the CDN. 52 | 53 | This update was automatically generated by the dependency update workflow. 54 | branch: dependency-update/amazon-ivs-player 55 | delete-branch: true 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon IVS Clip web demo 2 | 3 | A demo web application intended as an educational tool for demonstrating how you can implement a stream "clipping" functionality with [Amazon IVS](https://aws.amazon.com/ivs/), store those clips on [Amazon S3](https://aws.amazon.com/s3/) and serve them with [Amazon CloudFront](https://aws.amazon.com/cloudfront/). 4 | This demo uses [AWS Cloud Development Kit](https://aws.amazon.com/cdk/) (AWS CDK). 5 | 6 | Amazon IVS clip demo 7 | 8 | **This project is intended for education purposes only and not for production usage.** 9 | 10 | ## Prerequisites 11 | - AWS CLI ( [Installing the AWS CLI version 2](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) ) 12 | - Go ( [Download and install](https://golang.org/doc/install) ) 13 | - NodeJS ( [Installing Node.js](https://nodejs.org/) ) 14 | 15 | ## To use and deploy this project 16 | ***IMPORTANT NOTE:** this demo will create and use AWS resources on your AWS account, which will cost money.* 17 | 18 | 1. In the `cdk` directory, run: 19 | `npm install` 20 | 21 | 2. then run: 22 | `make bootstrap` 23 | 24 | 3. and finally: 25 | `make deploy` 26 | 27 | 28 | ### Final steps 29 | 30 | The script will give you 3 important pieces of information: 31 | 1. `ClipsUI.ingestserver`, the ingest server address to use in your broadcasting software ( [learn how to stream to Amazon IVS](https://aws.amazon.com/blogs/media/setting-up-for-streaming-with-amazon-ivs/) ) 32 | 2. `ClipsUI.streamkey`, the stream key for your newly created Amazon IVS channel 33 | 3. `ClipsUI.url`, the hosted frontend URL 34 | 35 | Once you go live, you will be able to see your live video stream on the hosted frontend, and you will be able to start creating clips. 36 | 37 | ## About Amazon IVS 38 | Amazon Interactive Video Service (Amazon IVS) is a managed live streaming solution that is quick and easy to set up, and ideal for creating interactive video experiences. [Learn more](https://aws.amazon.com/ivs/). 39 | 40 | * [Amazon IVS docs](https://docs.aws.amazon.com/ivs/) 41 | * [User Guide](https://docs.aws.amazon.com/ivs/latest/userguide/) 42 | * [API Reference](https://docs.aws.amazon.com/ivs/latest/APIReference/) 43 | * [Setting Up for Streaming with Amazon Interactive Video Service](https://aws.amazon.com/blogs/media/setting-up-for-streaming-with-amazon-ivs/) 44 | * [Learn more about Amazon IVS on IVS.rocks](https://ivs.rocks/) 45 | * [View more demos like this](https://ivs.rocks/examples) 46 | 47 | ## Security 48 | 49 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 50 | 51 | ## License 52 | 53 | This library is licensed under the MIT-0 License. See the LICENSE file. -------------------------------------------------------------------------------- /cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/cdk.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 54 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 55 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 56 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 57 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 58 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 59 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 60 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 61 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 62 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /clips/hls/hls.go: -------------------------------------------------------------------------------- 1 | package hls 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "sort" 9 | 10 | "github.com/grafov/m3u8" 11 | ) 12 | 13 | type Segment struct { 14 | SequenceID uint64 15 | Data []byte 16 | } 17 | 18 | func GetSegments(playbackURL string) ([]Segment, error) { 19 | 20 | masterManifest, err := getMasterManifest(playbackURL) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | topVariant := extractTopVariant(masterManifest.Variants) 26 | 27 | playlist, err := getPlaylist(topVariant.URI) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return downloadSegments(playlist.Segments) 33 | 34 | } 35 | 36 | func getMasterManifest(playbackURL string) (*m3u8.MasterPlaylist, error) { 37 | resp, err := http.Get(playbackURL) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | defer resp.Body.Close() 43 | 44 | p, listType, err := m3u8.DecodeFrom(resp.Body, true) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | switch listType { 50 | case m3u8.MASTER: 51 | masterPlaylist := p.(*m3u8.MasterPlaylist) 52 | return masterPlaylist, nil 53 | } 54 | 55 | return nil, fmt.Errorf("Could not decode master manifest.") 56 | } 57 | 58 | func getPlaylist(playlistURL string) (*m3u8.MediaPlaylist, error) { 59 | resp, err := http.Get(playlistURL) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | defer resp.Body.Close() 65 | 66 | p, listType, err := m3u8.DecodeFrom(resp.Body, true) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | switch listType { 72 | case m3u8.MEDIA: 73 | mediaPlaylist := p.(*m3u8.MediaPlaylist) 74 | return mediaPlaylist, nil 75 | } 76 | 77 | return nil, fmt.Errorf("Could not decode playlist.") 78 | } 79 | 80 | func extractTopVariant(v []*m3u8.Variant) m3u8.Variant { 81 | dm := &m3u8.Variant{} 82 | for _, v := range v { 83 | if v.Bandwidth > dm.Bandwidth { 84 | dm = v 85 | } 86 | } 87 | return *dm 88 | 89 | } 90 | 91 | func downloadSegments(segments []*m3u8.MediaSegment) ([]Segment, error) { 92 | 93 | var buffer []Segment 94 | numSegments := 0 95 | for _, s := range segments { 96 | if s != nil { 97 | numSegments++ 98 | } 99 | } 100 | 101 | done := make(chan Segment, numSegments) 102 | errorChan := make(chan error, numSegments) 103 | 104 | for _, segment := range segments { 105 | if segment != nil { 106 | go func(URI string, seq uint64) { 107 | s, err := downloadSegment(URI, seq) 108 | if err != nil { 109 | errorChan <- err 110 | done <- s 111 | return 112 | } 113 | done <- s 114 | errorChan <- nil 115 | }(segment.URI, segment.SeqId) 116 | } 117 | } 118 | 119 | var errStr string 120 | for i := 0; i < numSegments; i++ { 121 | buffer = append(buffer, <-done) 122 | if err := <-errorChan; err != nil { 123 | errStr = errStr + " " + err.Error() 124 | } 125 | } 126 | if errStr != "" { 127 | return nil, errors.New(errStr) 128 | } 129 | 130 | sort.SliceStable(buffer, func(i, j int) bool { 131 | return buffer[i].SequenceID < buffer[j].SequenceID 132 | }) 133 | 134 | return buffer, nil 135 | 136 | } 137 | 138 | func downloadSegment(URI string, sequenceID uint64) (Segment, error) { 139 | resp, err := http.Get(URI) 140 | if err != nil { 141 | return Segment{}, err 142 | } 143 | defer resp.Body.Close() 144 | if resp.StatusCode != http.StatusOK { 145 | return Segment{}, errors.New(resp.Status) 146 | } 147 | body, err := ioutil.ReadAll(resp.Body) 148 | if err != nil { 149 | return Segment{}, err 150 | } 151 | return Segment{ 152 | SequenceID: sequenceID, 153 | Data: body, 154 | }, nil 155 | } 156 | -------------------------------------------------------------------------------- /cdk/lib/backend-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import * as s3 from 'aws-cdk-lib/aws-s3'; 3 | import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; 4 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 5 | import * as iam from 'aws-cdk-lib/aws-iam'; 6 | import * as apigateway from 'aws-cdk-lib/aws-apigatewayv2'; 7 | import * as apigatewayintegrations from 'aws-cdk-lib/aws-apigatewayv2-integrations'; 8 | import * as sam from 'aws-cdk-lib/aws-sam'; 9 | import * as ivs from 'aws-cdk-lib/aws-ivs'; 10 | import { Construct } from 'constructs'; 11 | 12 | export interface StackProps { 13 | cdkProps: cdk.StackProps; 14 | } 15 | 16 | export class BackendStack extends cdk.Stack { 17 | public bucket: s3.Bucket; 18 | public cfDistro: cloudfront.CloudFrontWebDistribution; 19 | public channel: ivs.CfnChannel; 20 | public streamkey: ivs.CfnStreamKey; 21 | 22 | constructor(scope: Construct, id: string, props: StackProps) { 23 | super(scope, id, props.cdkProps); 24 | 25 | // Lets start by creating the S3 bucket where we will store the captured clips. 26 | this.bucket = new s3.Bucket(this, "clips-lambda-bucket", { 27 | websiteIndexDocument: 'index.html', 28 | }) 29 | 30 | // For Cloudfront to access the bucket, we need to create an Origin Access Identity that CloudFront uses to identify itself 31 | let oai = new cloudfront.OriginAccessIdentity(this, "clips-access"); 32 | 33 | // Instead of exposing an S3 bucket publicly (bad), this creates a CloudFront distribution with an S3 Origin Access Identity. 34 | this.cfDistro = new cloudfront.CloudFrontWebDistribution(this, 'clips-cdn', { 35 | originConfigs: [{ 36 | behaviors: [{ isDefaultBehavior: true }], 37 | s3OriginSource: { 38 | s3BucketSource: this.bucket, 39 | originAccessIdentity: oai, 40 | }, 41 | }] 42 | }); 43 | 44 | // clips depends on another published lambda layer for FFmpeg. Lets deploy this layer as a nested stack into our application. 45 | // Once that is done we capture the output layer version and create a new Lambda Layer based on it for our application. 46 | let ffmpegLayerSamApp = new sam.CfnApplication(this, 'FFmpegLayer', { 47 | location: { 48 | applicationId: 'arn:aws:serverlessrepo:us-east-1:145266761615:applications/ffmpeg-lambda-layer', 49 | semanticVersion: '1.0.0' 50 | } 51 | }); 52 | 53 | let layerVersionArn = cdk.Stack.of(this).resolve(ffmpegLayerSamApp.getAtt('Outputs.LayerVersion')) 54 | let ffmpegLayer = lambda.LayerVersion.fromLayerVersionArn(this, 'FFmpegLayerVersion', layerVersionArn) 55 | 56 | // We can now go ahead and create the Lambda that will generate clips. We pass in the bucket and url so it can return the proper clip path. 57 | let lambdaFunction = new lambda.Function(this, "clips-backend", { 58 | runtime: lambda.Runtime.GO_1_X, 59 | memorySize: 512, 60 | timeout: cdk.Duration.minutes(5), 61 | handler: 'bin/clips', 62 | layers: [ffmpegLayer], 63 | code: lambda.Code.fromAsset('../bin/clips.zip'), 64 | environment: { 65 | "bucket": this.bucket.bucketName, 66 | "url": this.cfDistro.distributionDomainName 67 | } 68 | }) 69 | 70 | // The Lambda function needs the ability to write files to S3, so lets grant it PUT access to our bucket. 71 | lambdaFunction.addToRolePolicy(new iam.PolicyStatement({ 72 | sid: "clipswriteaccess", 73 | effect: iam.Effect.ALLOW, 74 | actions: [ 75 | "s3:PutObject" 76 | ], 77 | resources: [ 78 | this.bucket.bucketArn, 79 | this.bucket.arnForObjects("*"), 80 | ] 81 | })) 82 | 83 | // Our frontend needs a way of calling the Lambda function so lets use a API Gateway in front of the Lambda with a proxy integration for the routes. 84 | let gw = new apigateway.HttpApi(this, "clips-gateway", { 85 | apiName: "clips-gw", 86 | }) 87 | 88 | gw.addRoutes({ 89 | path: '/clip', 90 | methods: [ apigateway.HttpMethod.GET ], 91 | integration: new apigatewayintegrations.HttpLambdaIntegration('clip-lambdaintegration', lambdaFunction), 92 | }); 93 | 94 | // Create the IVS channel 95 | this.channel = new ivs.CfnChannel(this, 'clip-channel', { 96 | latencyMode: 'LOW', 97 | name: "clip-channel", 98 | }) 99 | 100 | this.streamkey = new ivs.CfnStreamKey(this, "clip-streamkey", { 101 | channelArn: this.channel.ref, 102 | }) 103 | 104 | new cdk.CfnOutput(this, 'playbackUrl', { value: this.channel.attrPlaybackUrl, exportName: "play" } ); 105 | new cdk.CfnOutput(this, 'api', { value: gw.apiEndpoint, exportName: "api2" }); 106 | 107 | 108 | } 109 | } 110 | 111 | -------------------------------------------------------------------------------- /clips/clips.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "io/ioutil" 8 | "os" 9 | "path" 10 | "regexp" 11 | 12 | "github.com/aws-samples/amazon-ivs-clip-web-demo/clips/hls" 13 | "github.com/aws-samples/amazon-ivs-clip-web-demo/clips/remux" 14 | "github.com/aws/aws-lambda-go/events" 15 | "github.com/aws/aws-lambda-go/lambda" 16 | "github.com/aws/aws-sdk-go/aws" 17 | "github.com/aws/aws-sdk-go/aws/session" 18 | "github.com/aws/aws-sdk-go/service/s3/s3manager" 19 | "github.com/google/uuid" 20 | ) 21 | 22 | type Response struct { 23 | URL string `json:",omitempty"` 24 | Error string `json:",omitempty"` 25 | } 26 | 27 | //Regex to verify that the clip requests is in the same AWS account as the lambda. 28 | var channelPattern = regexp.MustCompile(`^https:\/\/[a-z0-9]+\.([a-z0-9-]+)\.playback.live-video.net\/api\/video\/v[0-9]\/([a-z0-9-]+)\.([0-9]+)\.channel\.([a-zA-Z0-9]+)\.m3u8$`) 29 | 30 | func main() { 31 | lambda.Start(clip) 32 | } 33 | 34 | func clip(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 35 | 36 | /////////// 37 | // Sanity check the input 38 | /////////// 39 | 40 | //Check if request has "channel" parameter 41 | var channel string 42 | if val, ok := request.QueryStringParameters["channel"]; ok { 43 | channel = val 44 | } else { 45 | return events.APIGatewayProxyResponse{ 46 | StatusCode: 400, 47 | Headers: map[string]string{"Access-Control-Allow-Origin": "*"}, 48 | Body: encodeErrorResponse("Please provide a channel"), 49 | }, nil 50 | } 51 | 52 | //Does the URL match the pattern? 53 | if !channelPattern.MatchString(channel) { 54 | return events.APIGatewayProxyResponse{ 55 | StatusCode: 400, 56 | Headers: map[string]string{"Access-Control-Allow-Origin": "*"}, 57 | Body: encodeErrorResponse("Please provide an accepted playback url"), 58 | }, nil 59 | } 60 | 61 | //Extract the AWS Account ID and make sure it matches the lambda is currently running in. 62 | match := channelPattern.FindStringSubmatch(channel) 63 | if request.RequestContext.AccountID != match[3] { 64 | return events.APIGatewayProxyResponse{ 65 | StatusCode: 400, 66 | Headers: map[string]string{"Access-Control-Allow-Origin": "*"}, 67 | Body: encodeErrorResponse("Please provide an allowlisted playback url"), 68 | }, nil 69 | } 70 | 71 | /////////// 72 | // Perform the segment download & stitch 73 | /////////// 74 | 75 | // Download the segments for a given manifest URL. 76 | segments, err := hls.GetSegments(channel) 77 | if err != nil { 78 | return events.APIGatewayProxyResponse{ 79 | StatusCode: 500, 80 | Headers: map[string]string{"Access-Control-Allow-Origin": "*"}, 81 | Body: encodeErrorResponse("Could not clip."), 82 | }, nil 83 | } 84 | 85 | // Stitch together the segments using FFmpeg and write a new .mp4 into /tmp 86 | filename := uuid.New().String() + ".mp4" 87 | tempPath := path.Join("/tmp/" + filename) 88 | err = remux.Remux(segments, tempPath) 89 | if err != nil { 90 | return events.APIGatewayProxyResponse{ 91 | StatusCode: 500, 92 | Headers: map[string]string{"Access-Control-Allow-Origin": "*"}, 93 | Body: encodeErrorResponse("Could not clip."), 94 | }, nil 95 | } 96 | 97 | /////////// 98 | // Upload the segement to S3 99 | /////////// 100 | 101 | // Read generated .mp4 back into memory 102 | mp4, err := ioutil.ReadFile(tempPath) 103 | if err != nil { 104 | return events.APIGatewayProxyResponse{ 105 | StatusCode: 500, 106 | Headers: map[string]string{"Access-Control-Allow-Origin": "*"}, 107 | Body: encodeErrorResponse("Could not clip."), 108 | }, nil 109 | } 110 | 111 | // Upload file to S3 from memory 112 | sess := session.Must(session.NewSession()) 113 | uploader := s3manager.NewUploader(sess) 114 | _, err = uploader.Upload(&s3manager.UploadInput{ 115 | Bucket: aws.String(os.Getenv("bucket")), 116 | Key: aws.String(path.Join("clips/", filename)), 117 | Body: bytes.NewReader(mp4), 118 | }) 119 | 120 | if err != nil { 121 | return events.APIGatewayProxyResponse{ 122 | StatusCode: 500, 123 | Headers: map[string]string{"Access-Control-Allow-Origin": "*"}, 124 | Body: encodeErrorResponse("Could not clip."), 125 | }, nil 126 | } 127 | 128 | os.Remove(tempPath) 129 | 130 | // Respond with URL to clip 131 | 132 | rs := Response{ 133 | URL: "https://" + path.Join(os.Getenv("url"), "clips/", filename), 134 | } 135 | 136 | resp, _ := json.Marshal(rs) 137 | 138 | return events.APIGatewayProxyResponse{ 139 | StatusCode: 200, 140 | Headers: map[string]string{"Access-Control-Allow-Origin": "*"}, 141 | Body: string(resp), 142 | }, nil 143 | 144 | } 145 | 146 | func encodeErrorResponse(err string) string { 147 | rs := Response{ 148 | Error: err, 149 | } 150 | 151 | resp, _ := json.Marshal(rs) 152 | return string(resp) 153 | 154 | } 155 | -------------------------------------------------------------------------------- /web-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Amazon IVS - Clip demo 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 | 39 | 51 | 56 | 64 |
65 |
66 |
67 |
68 | 69 | 70 |
71 |
72 | 73 |
74 |
75 |
76 |
77 | 78 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /web-ui/clip.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | // Configuration 20 | let playbackUrl = ''; 21 | let clipEndpoint = ''; 22 | 23 | let startApp = function(){ 24 | // Elements 25 | const playerOverlay = document.getElementById("overlay"); 26 | const playerControls = document.getElementById("player-controls"); 27 | const btnPlay = document.getElementById("play"); 28 | const btnMute = document.getElementById("mute"); 29 | const btnClip = document.getElementById("clip"); 30 | const btnSettings = document.getElementById("settings"); 31 | const settingsMenu = document.getElementById("settings-menu"); 32 | const clipsEls = document.getElementById("clips-els"); 33 | const clipTemplate = document.getElementById("clip-template"); 34 | const clipIcon = document.getElementById("clip_icon"); 35 | const clipSpinner = document.getElementById("clip_spinner"); 36 | 37 | // App 38 | const videoPlayer = document.getElementById("video-player"); 39 | const clientId = `${Math.random().toString().slice(2)}${Math.random().toString().slice(2)}`; 40 | 41 | // Btn icons 42 | let setBtnPaused = function(){ 43 | btnPlay.classList.remove("btn--play"); 44 | btnPlay.classList.add("btn--pause"); 45 | }; 46 | 47 | let setBtnPlay = function(){ 48 | btnPlay.classList.add("btn--play"); 49 | btnPlay.classList.remove("btn--pause"); 50 | }; 51 | 52 | let setBtnMute = function(){ 53 | btnMute.classList.remove("btn--mute"); 54 | btnMute.classList.add("btn--unmute"); 55 | }; 56 | 57 | let setBtnUnmute = function(){ 58 | btnMute.classList.add("btn--mute"); 59 | btnMute.classList.remove("btn--unmute"); 60 | }; 61 | 62 | // Player 63 | (function (IVSPlayer) { 64 | const PlayerState = IVSPlayer.PlayerState; 65 | const PlayerEventType = IVSPlayer.PlayerEventType; 66 | 67 | // Initialize player 68 | const player = IVSPlayer.create(); 69 | player.attachHTMLVideoElement(videoPlayer); 70 | 71 | player.addEventListener(PlayerEventType.TEXT_METADATA_CUE, function (cue) { 72 | const metadataText = cue.text; 73 | const position = player.getPosition().toFixed(2); 74 | console.log( 75 | `Player Event - TEXT_METADATA_CUE: "${metadataText}". Observed ${position}s after playback started.` 76 | ); 77 | }); 78 | 79 | player.addEventListener(PlayerEventType.AUDIO_BLOCKED, function(){ 80 | setBtnMute(); 81 | }); 82 | 83 | // Setup stream and play 84 | player.setAutoplay(true); 85 | player.load(playbackUrl); 86 | 87 | // Controls events 88 | // Play/Pause 89 | btnPlay.addEventListener( 90 | "click", 91 | function (e) { 92 | if (btnPlay.classList.contains("btn--play")) { 93 | // change to pause 94 | setBtnPaused(); 95 | player.pause(); 96 | } else { 97 | // change to play 98 | setBtnPlay(); 99 | player.play(); 100 | } 101 | }, 102 | false 103 | ); 104 | 105 | // Mute/Unmute 106 | btnMute.addEventListener( 107 | "click", 108 | function (e) { 109 | if (btnMute.classList.contains("btn--mute")) { 110 | setBtnMute(); 111 | player.setMuted(1); 112 | } else { 113 | setBtnUnmute(); 114 | player.setMuted(0); 115 | } 116 | }, 117 | false 118 | ); 119 | 120 | // Create Quality Options 121 | let createQualityOptions = function (obj, i) { 122 | let q = document.createElement("a"); 123 | let qText = document.createTextNode(obj.name); 124 | settingsMenu.appendChild(q); 125 | q.classList.add("settings-menu-item"); 126 | q.appendChild(qText); 127 | 128 | q.addEventListener("click", (event) => { 129 | player.setQuality(obj); 130 | return false; 131 | }); 132 | }; 133 | 134 | // Close Settings menu 135 | let closeSettingsMenu = function () { 136 | btnSettings.classList.remove("btn--settings-on"); 137 | btnSettings.classList.add("btn--settings-off"); 138 | settingsMenu.classList.remove("open"); 139 | }; 140 | 141 | // Settings 142 | btnSettings.addEventListener( 143 | "click", 144 | function (e) { 145 | let qualities = player.getQualities(); 146 | let currentQuality = player.getQuality(); 147 | 148 | // Empty Settings menu 149 | while (settingsMenu.firstChild) 150 | settingsMenu.removeChild(settingsMenu.firstChild); 151 | 152 | if (btnSettings.classList.contains("btn--settings-off")) { 153 | for (var i = 0; i < qualities.length; i++) { 154 | createQualityOptions(qualities[i], i); 155 | } 156 | btnSettings.classList.remove("btn--settings-off"); 157 | btnSettings.classList.add("btn--settings-on"); 158 | settingsMenu.classList.add("open"); 159 | } else { 160 | closeSettingsMenu(); 161 | } 162 | }, 163 | false 164 | ); 165 | 166 | // Close Settings menu if user clicks outside the player 167 | window.addEventListener("click", function (e) { 168 | if (playerOverlay.contains(e.target)) { 169 | } else { 170 | closeSettingsMenu(); 171 | } 172 | }); 173 | 174 | // Clip 175 | btnClip.addEventListener( 176 | "click", 177 | function (e) { 178 | createClip(); 179 | }, 180 | false 181 | ); 182 | 183 | // Create Clip 184 | let createClip = function (time) { 185 | const url = clipEndpoint; 186 | 187 | clipIcon.classList.add("hidden"); 188 | clipSpinner.classList.remove("hidden"); 189 | 190 | var call = new XMLHttpRequest(); 191 | call.responseType = "json"; 192 | call.open("GET", url, true); 193 | call.onload = function () { 194 | clipSrc = call.response.URL; 195 | displayClip(clipSrc); 196 | clipIcon.classList.remove("hidden"); 197 | clipSpinner.classList.add("hidden"); 198 | }; 199 | call.send("null"); 200 | }; 201 | 202 | // Copy btn 203 | let copybtnClickHandler = function(src) { 204 | navigator.clipboard.writeText(src).then(function() { 205 | alert("Copied to clipboard!"); 206 | }, function(err) { 207 | console.error('Failed to copy!', err); 208 | }); 209 | }; 210 | 211 | // Share btn 212 | let sharebtnClickHandler = function(src) { 213 | navigator.share({ 214 | title: 'Amazon IVS Clip', 215 | text: 'Check out the clip I made!', 216 | url: src, 217 | }); 218 | }; 219 | 220 | // Open btn 221 | let openbtnClickHandler = function(src) { 222 | window.open(src, '_blank'); 223 | }; 224 | 225 | // Handle Clip play 226 | let handleClipPlay = function(){ 227 | setBtnMute(); 228 | setBtnPaused(); 229 | player.setMuted(1); 230 | player.pause(); 231 | }; 232 | 233 | // Display Clip 234 | let displayClip = function (src) { 235 | let receptacle = clipsEls; 236 | let template = clipTemplate; 237 | let clone = template.content.cloneNode(true); 238 | let clone_player = clone.querySelectorAll('.clip-el__player'); 239 | let clone_copy_btn = clone.querySelectorAll('.clip-el__copy'); 240 | let clone_share_btn = clone.querySelectorAll('.clip-el__share'); 241 | let clone_open_btn = clone.querySelectorAll('.clip-el__open'); 242 | 243 | // pause all clips 244 | document.querySelectorAll('.clip-el__player').forEach(clip => clip.pause()); 245 | 246 | // create player 247 | clone_player[0].setAttribute("src", src); 248 | clone_player[0].load(); 249 | clone_player[0].pause(); 250 | clone_player[0].onplay = function(e){handleClipPlay()}; 251 | 252 | // copy to clipboard 253 | clone_copy_btn[0].addEventListener("click", function(e){copybtnClickHandler(src)}, false); 254 | 255 | // share 256 | if (navigator.canShare){ 257 | clone_share_btn[0].classList.remove("hidden"); 258 | clone_share_btn[0].addEventListener("click", function(e){sharebtnClickHandler(src)}, false); 259 | } 260 | 261 | // open 262 | clone_open_btn[0].addEventListener("click", function(e){openbtnClickHandler(src)}, false); 263 | 264 | // append 265 | receptacle.appendChild(clone); 266 | 267 | }; 268 | 269 | })(window.IVSPlayer); 270 | } 271 | 272 | // Update values 273 | fetch('config.json') 274 | .then(response => response.json()) 275 | .then(data => { 276 | playbackUrl = data.playbackURL; 277 | clipEndpoint = data.api + '/clip?channel='+ playbackUrl; 278 | startApp(); 279 | }); -------------------------------------------------------------------------------- /web-ui/style.css: -------------------------------------------------------------------------------- 1 | /* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. */ 2 | /* SPDX-License-Identifier: MIT-0 */ 3 | 4 | /* Reset */ 5 | *,*::before,*::after{box-sizing:border-box}ul[class],ol[class]{padding:0}body,h1,h2,h3,h4,h5,p,ul[class],ol[class],figure,blockquote,dl,dd{margin:0}html{scroll-behavior:smooth}body{min-height:100vh;text-rendering:optimizeSpeed;line-height:1.5}ul[class],ol[class]{list-style:none}a:not([class]){text-decoration-skip-ink:auto}img{max-width:100%;display:block}article>*+*{margin-top:1em}input,button,textarea,select{font:inherit}@media (prefers-reduced-motion:reduce){*{animation-duration:0.01ms!important;animation-iteration-count:1!important;transition-duration:0.01ms!important;scroll-behavior:auto!important}} 6 | 7 | /* Variables */ 8 | :root { 9 | /* Sizes */ 10 | --radius: 12px; 11 | --btn-size: 3rem; 12 | --btn-round-size: 2.8rem; 13 | 14 | /* Font */ 15 | --text-color-light: #000; 16 | --text-alt-color-light: #777; 17 | --text-color-dark: #fff; 18 | --text-alt-color-dark: #777; 19 | 20 | /* Background */ 21 | --bg-color-light: hsl(213, 7%, 70%); 22 | --bg-alt-color-light: hsl(213, 7%, 90%); 23 | --bg-alt2-color-light: hsl(213, 7%, 78%); 24 | 25 | --bg-color-dark: hsl(213, 20%, 15%); 26 | --bg-alt-color-dark: hsl(213, 20%, 10%); 27 | --bg-alt2-color-dark: hsl(213, 20%, 25%); 28 | 29 | /* Buttons */ 30 | --btn-color-fill-light: hsl(213, 10%, 10%); 31 | --btn-color-fill-dark: hsl(213, 10%, 80%); 32 | --btn-color-highlight: hsl(213, 55%, 30%); 33 | --btn-color-highlight-hover: hsl(213, 55%, 40%); 34 | 35 | /* Theme */ 36 | --bg-color-1: var(--bg-color-light); 37 | --bg-color-2: var(--bg-alt-color-light); 38 | --bg-color-3: var(--bg-alt2-color-light); 39 | 40 | --btn-color-fill: var(--btn-color-fill-light); 41 | } 42 | 43 | /* Dark theme */ 44 | @media (prefers-color-scheme: dark) { 45 | :root { 46 | --bg-color-1: var(--bg-alt-color-dark); 47 | --bg-color-2: var(--bg-color-dark); 48 | --bg-color-3: var(--bg-alt2-color-dark); 49 | 50 | --btn-color-fill: var(--btn-color-fill-dark); 51 | } 52 | } 53 | 54 | /* Style */ 55 | html, 56 | body { 57 | width: 100%; 58 | height: 100%; 59 | margin: 0; 60 | padding: 0; 61 | overflow: hidden; 62 | } 63 | 64 | body { 65 | overflow: hidden; 66 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif; 67 | user-select: none; 68 | background: var(--bg-color-1); 69 | display: grid; 70 | } 71 | 72 | #app { 73 | height: 100vh; 74 | display: grid; 75 | grid-auto-flow: column; 76 | grid-template-columns: [player] 1fr [clips] 340px; 77 | overflow: hidden; 78 | } 79 | 80 | .hidden { 81 | display: none !important; 82 | } 83 | 84 | /* App sections */ 85 | section { 86 | height: 100vh; 87 | display: grid; 88 | position: relative; 89 | } 90 | 91 | .player-section { 92 | grid-area: player; 93 | } 94 | 95 | .clips-section { 96 | grid-area: clips; 97 | background: var(--bg-color-2); 98 | overflow-y: auto; 99 | padding: 1.5rem; 100 | display: grid; 101 | gap: 2rem; 102 | } 103 | 104 | .clips-els { 105 | display: flex; 106 | flex-direction: column-reverse; 107 | justify-content: flex-end; 108 | gap: 2rem; 109 | } 110 | 111 | .ivs-player { 112 | width: 100%; 113 | height: 100%; 114 | } 115 | 116 | /* Player overlay */ 117 | #overlay { 118 | position: absolute; 119 | top: 0; 120 | left: 0; 121 | right: 0; 122 | bottom: 0; 123 | z-index: 1; 124 | } 125 | 126 | .overlay .icon { 127 | position: absolute; 128 | width: 40px; 129 | height: 40px; 130 | font-size: 36px; 131 | animation: scale 0.2s ease-in-out; 132 | transition: all 0.15s ease-in-out; 133 | filter: drop-shadow(0 6px 15px rgba(0, 0, 0, 0.2)); 134 | } 135 | 136 | .overlay .icon.fade { 137 | opacity: 0; 138 | } 139 | 140 | #player-controls { 141 | position: absolute; 142 | top: 0; 143 | left: 0; 144 | right: 0; 145 | bottom: 0; 146 | z-index: 1; 147 | transition: background 0.1s ease-in-out; 148 | } 149 | 150 | .player-controls__inner { 151 | height: var(--btn-size); 152 | position: absolute; 153 | bottom: 0; 154 | left: 0; 155 | right: 0; 156 | display: flex; 157 | padding: 0 10px 10px 10px; 158 | } 159 | 160 | .player--hover #player-controls { 161 | background: linear-gradient(0deg, 162 | rgba(0, 0, 0, 0.7) 0%, 163 | rgba(0, 0, 0, 0) 20%, 164 | rgba(0, 0, 0, 0) 100%); 165 | } 166 | 167 | .player--hover .btn { 168 | display: flex; 169 | } 170 | 171 | .player--hover #settings-menu.open { 172 | display: block; 173 | } 174 | 175 | .btn { 176 | outline: none; 177 | appearance: none; 178 | cursor: pointer; 179 | border: 2px solid transparent; 180 | -webkit-appearance: none; 181 | background: transparent; 182 | position: absolute; 183 | bottom: 10px; 184 | padding: 0; 185 | display: flex; 186 | flex-shrink: 0; 187 | flex-wrap: nowrap; 188 | display: none; 189 | border-radius: 2px; 190 | } 191 | 192 | .btn--icon { 193 | width: var(--btn-size); 194 | height: var(--btn-size); 195 | justify-content: center; 196 | align-items: center; 197 | border-radius: var(--btn-size); 198 | } 199 | 200 | .btn:focus { 201 | border: 2px solid #fff; 202 | } 203 | 204 | .btn--clip { 205 | background: var(--btn-color-highlight); 206 | transition: all .15s ease-in-out; 207 | } 208 | 209 | .btn--clip:hover, 210 | .btn--clip:focus, 211 | .btn--clip:active { 212 | background: var(--btn-color-highlight-hover); 213 | } 214 | 215 | .btn--clip:active { 216 | transform: translate3d(0, 2px, 0); 217 | } 218 | 219 | .btn-tooltip { 220 | position: absolute; 221 | bottom: calc(100% + 1rem); 222 | display: block; 223 | text-align: center; 224 | background: var(--btn-color-highlight-hover); 225 | border-radius: var(--radius); 226 | padding: 0.5rem 1rem; 227 | color: #FFF; 228 | font-weight: 500; 229 | font-size: .85rem; 230 | transition: all .15s ease-in-out; 231 | } 232 | 233 | .btn-tooltip:after { 234 | content: ''; 235 | width: 0; 236 | height: 0; 237 | border-style: solid; 238 | border-width: 6px 5px 0 5px; 239 | border-color: var(--btn-color-highlight-hover) transparent transparent transparent; 240 | position: absolute; 241 | bottom: -6px; 242 | left: 50%; 243 | margin-left: -5px; 244 | } 245 | 246 | .btn--clip:hover .btn-tooltip, 247 | .btn--clip:focus .btn-tooltip { 248 | bottom: calc(100% + 1.3rem); 249 | } 250 | 251 | .icon { 252 | fill: #fff; 253 | } 254 | 255 | .spinner { 256 | display: inline-block; 257 | width: 20px; 258 | height: 20px; 259 | border: 3px solid rgba(255,255,255,.3); 260 | border-radius: 50%; 261 | border-top-color: #fff; 262 | animation: spin 1s ease-in-out infinite; 263 | } 264 | 265 | @keyframes spin { 266 | to { transform: rotate(360deg); } 267 | } 268 | 269 | #play { 270 | left: 15px; 271 | } 272 | 273 | #mute { 274 | left: calc(25px + var(--btn-size)); 275 | } 276 | 277 | #clip { 278 | right: calc(25px + var(--btn-size)); 279 | } 280 | 281 | #settings { 282 | right: 15px; 283 | } 284 | 285 | .icon--settings { 286 | transition: transform 0.15s ease-in-out; 287 | } 288 | 289 | .btn--pause .icon--pause { 290 | display: none; 291 | } 292 | 293 | .btn--play .icon--play { 294 | display: none; 295 | } 296 | 297 | .btn--mute .icon--volume_off { 298 | display: none; 299 | } 300 | 301 | .btn--unmute .icon--volume_up { 302 | display: none; 303 | } 304 | 305 | .btn--settings-on .icon--settings { 306 | transform: rotate(45deg); 307 | } 308 | 309 | #settings-menu { 310 | width: 180px; 311 | height: auto; 312 | padding: 20px 0; 313 | position: absolute; 314 | right: 10px; 315 | bottom: 55px; 316 | background: #000; 317 | z-index: 2; 318 | display: none; 319 | border-radius: 10px; 320 | color: #fff; 321 | } 322 | 323 | .settings-menu-item { 324 | width: 100%; 325 | height: 40px; 326 | line-height: 40px; 327 | padding: 0 20px; 328 | display: block; 329 | cursor: pointer; 330 | } 331 | 332 | .settings-menu-item:hover { 333 | background: rgba(255, 255, 255, 0.1); 334 | } 335 | 336 | 337 | /* Clips */ 338 | .clip-el { 339 | transition: all 0.35s ease-in-out; 340 | animation: fadeIn 0.5s 1; 341 | border-radius: var(--radius); 342 | } 343 | 344 | @keyframes fadeIn { 345 | 0% { 346 | opacity: 0.2; 347 | background: var(--btn-color-highlight-hover); 348 | } 349 | 100% { 350 | opacity: 1; 351 | background: transparent; 352 | } 353 | } 354 | 355 | .clip-el__player { 356 | width: 100%; 357 | background: #000; 358 | border-radius: var(--radius); 359 | } 360 | 361 | .clip-el__btns { 362 | margin: 0.2rem 0 0 0; 363 | display: flex; 364 | justify-content: center; 365 | gap: 1rem; 366 | } 367 | 368 | .btn-round { 369 | width: var(--btn-round-size); 370 | height: var(--btn-round-size); 371 | border-radius: var(--btn-round-size); 372 | background: var(--bg-color-3); 373 | border: 0; 374 | appearance: none; 375 | -webkit-appearance: none; 376 | cursor: pointer; 377 | outline: none; 378 | border: 3px solid var(--bg-color-3); 379 | display: flex; 380 | justify-content: center; 381 | align-items: center; 382 | } 383 | 384 | .btn-round:hover, 385 | .btn-round:focus, 386 | .btn-round:active { 387 | background: var(--bg-color-1); 388 | } 389 | 390 | .btn-round:active { 391 | transform: translate3d(0, 3px, 0); 392 | } 393 | 394 | .btn-round svg { 395 | fill: var(--btn-color-fill); 396 | } 397 | 398 | 399 | @media (max-width: 840px) { 400 | section { 401 | height: 100%; 402 | } 403 | #app { 404 | grid-auto-flow: row; 405 | grid-template-columns: unset; 406 | grid-template-rows: [player] 0.5fr [clips] 1fr; 407 | } 408 | } -------------------------------------------------------------------------------- /clips/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= 3 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 4 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 5 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 6 | github.com/aws/aws-lambda-go v1.20.0 h1:ZSweJx/Hy9BoIDXKBEh16vbHH0t0dehnF8MKpMiOWc0= 7 | github.com/aws/aws-lambda-go v1.20.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= 8 | github.com/aws/aws-sdk-go v1.35.26 h1:MawRvDpAp/Ai859dPC1xo1fdU/BIkijoHj0DwXLXXkI= 9 | github.com/aws/aws-sdk-go v1.35.26/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k= 10 | github.com/aws/aws-sdk-go v1.35.30 h1:ZT+70Tw1ar5U2bL81ZyIvcLorxlD1UoxoIgjsEkismY= 11 | github.com/aws/aws-sdk-go v1.35.30/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k= 12 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 13 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 14 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 15 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 16 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 17 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 18 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= 22 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 23 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 24 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 25 | github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 26 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 27 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 28 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 29 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 30 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 31 | github.com/google/go-licenses v0.0.0-20201026145851-73411c8fa237 h1:qmrsmPqL7jK5f7dwLc4oDBZu/3pzB19tvhnP4TngNYY= 32 | github.com/google/go-licenses v0.0.0-20201026145851-73411c8fa237/go.mod h1:g1VOUGKZYIqe8lDq2mL7plhAWXqrEaGUs7eIjthN1sk= 33 | github.com/google/licenseclassifier v0.0.0-20190926221455-842c0d70d702 h1:nVgx26pAe6l/02mYomOuZssv28XkacGw/0WeiTVorqw= 34 | github.com/google/licenseclassifier v0.0.0-20190926221455-842c0d70d702/go.mod h1:qsqn2hxC+vURpyBRygGUuinTO42MFRLcsmQ/P8v94+M= 35 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= 36 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 37 | github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA= 38 | github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080= 39 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 40 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 41 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 42 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 43 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 44 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 45 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 46 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 47 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 48 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= 49 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 50 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 51 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 52 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 53 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 54 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 55 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 56 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 57 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 58 | github.com/otiai10/copy v1.2.0 h1:HvG945u96iNadPoG2/Ja2+AUJeW5YuFQMixq9yirC+k= 59 | github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= 60 | github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= 61 | github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= 62 | github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= 63 | github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= 64 | github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= 65 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 66 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 67 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 68 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 69 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 70 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 71 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 72 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 73 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 74 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 75 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 76 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 77 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 78 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 79 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 80 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 81 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 82 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 83 | github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= 84 | github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= 85 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 86 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 87 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 88 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 89 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 90 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 91 | github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 92 | github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= 93 | github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= 94 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 95 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 96 | golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 97 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 98 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 99 | golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f h1:kz4KIr+xcPUsI3VMoqWfPMvtnJ6MGfiVwsWSVzphMO4= 100 | golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 101 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 102 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 103 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 104 | golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 105 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= 106 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 107 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 108 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 109 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 110 | golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 111 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 112 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 113 | golang.org/x/sys v0.0.0-20191119060738-e882bf8e40c2 h1:wAW1U21MfVN0sUipAD8952TBjGXMRHFKQugDlQ9RwwE= 114 | golang.org/x/sys v0.0.0-20191119060738-e882bf8e40c2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 116 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 117 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 118 | golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= 119 | golang.org/x/tools v0.0.0-20191118222007-07fc4c7f2b98 h1:tZwpOHmF1OEL9wJGSgBALnhFg/8VKjQTtctCX51GLNI= 120 | golang.org/x/tools v0.0.0-20191118222007-07fc4c7f2b98/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 121 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 122 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 123 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 124 | gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= 125 | gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= 126 | gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= 127 | gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= 128 | gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= 129 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 130 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 131 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 132 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 133 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 134 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 135 | -------------------------------------------------------------------------------- /THIRD-PARTY-LICENSES.txt: -------------------------------------------------------------------------------- 1 | **Vector assets are used under Apache License 2.0 (reproduced below). 2 | 3 | Apache License 2.0 4 | 5 | 6 | Apache License 7 | Version 2.0, January 2004 8 | http://www.apache.org/licenses/ 9 | 10 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 11 | 12 | 1. Definitions. 13 | 14 | "License" shall mean the terms and conditions for use, reproduction, 15 | and distribution as defined by Sections 1 through 9 of this document. 16 | 17 | "Licensor" shall mean the copyright owner or entity authorized by 18 | the copyright owner that is granting the License. 19 | 20 | "Legal Entity" shall mean the union of the acting entity and all 21 | other entities that control, are controlled by, or are under common 22 | control with that entity. For the purposes of this definition, 23 | "control" means (i) the power, direct or indirect, to cause the 24 | direction or management of such entity, whether by contract or 25 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 26 | outstanding shares, or (iii) beneficial ownership of such entity. 27 | 28 | "You" (or "Your") shall mean an individual or Legal Entity 29 | exercising permissions granted by this License. 30 | 31 | "Source" form shall mean the preferred form for making modifications, 32 | including but not limited to software source code, documentation 33 | source, and configuration files. 34 | 35 | "Object" form shall mean any form resulting from mechanical 36 | transformation or translation of a Source form, including but 37 | not limited to compiled object code, generated documentation, 38 | and conversions to other media types. 39 | 40 | "Work" shall mean the work of authorship, whether in Source or 41 | Object form, made available under the License, as indicated by a 42 | copyright notice that is included in or attached to the work 43 | (an example is provided in the Appendix below). 44 | 45 | "Derivative Works" shall mean any work, whether in Source or Object 46 | form, that is based on (or derived from) the Work and for which the 47 | editorial revisions, annotations, elaborations, or other modifications 48 | represent, as a whole, an original work of authorship. For the purposes 49 | of this License, Derivative Works shall not include works that remain 50 | separable from, or merely link (or bind by name) to the interfaces of, 51 | the Work and Derivative Works thereof. 52 | 53 | "Contribution" shall mean any work of authorship, including 54 | the original version of the Work and any modifications or additions 55 | to that Work or Derivative Works thereof, that is intentionally 56 | submitted to Licensor for inclusion in the Work by the copyright owner 57 | or by an individual or Legal Entity authorized to submit on behalf of 58 | the copyright owner. For the purposes of this definition, "submitted" 59 | means any form of electronic, verbal, or written communication sent 60 | to the Licensor or its representatives, including but not limited to 61 | communication on electronic mailing lists, source code control systems, 62 | and issue tracking systems that are managed by, or on behalf of, the 63 | Licensor for the purpose of discussing and improving the Work, but 64 | excluding communication that is conspicuously marked or otherwise 65 | designated in writing by the copyright owner as "Not a Contribution." 66 | 67 | "Contributor" shall mean Licensor and any individual or Legal Entity 68 | on behalf of whom a Contribution has been received by Licensor and 69 | subsequently incorporated within the Work. 70 | 71 | 2. Grant of Copyright License. Subject to the terms and conditions of 72 | this License, each Contributor hereby grants to You a perpetual, 73 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 74 | copyright license to reproduce, prepare Derivative Works of, 75 | publicly display, publicly perform, sublicense, and distribute the 76 | Work and such Derivative Works in Source or Object form. 77 | 78 | 3. Grant of Patent License. Subject to the terms and conditions of 79 | this License, each Contributor hereby grants to You a perpetual, 80 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 81 | (except as stated in this section) patent license to make, have made, 82 | use, offer to sell, sell, import, and otherwise transfer the Work, 83 | where such license applies only to those patent claims licensable 84 | by such Contributor that are necessarily infringed by their 85 | Contribution(s) alone or by combination of their Contribution(s) 86 | with the Work to which such Contribution(s) was submitted. If You 87 | institute patent litigation against any entity (including a 88 | cross-claim or counterclaim in a lawsuit) alleging that the Work 89 | or a Contribution incorporated within the Work constitutes direct 90 | or contributory patent infringement, then any patent licenses 91 | granted to You under this License for that Work shall terminate 92 | as of the date such litigation is filed. 93 | 94 | 4. Redistribution. You may reproduce and distribute copies of the 95 | Work or Derivative Works thereof in any medium, with or without 96 | modifications, and in Source or Object form, provided that You 97 | meet the following conditions: 98 | 99 | (a) You must give any other recipients of the Work or 100 | Derivative Works a copy of this License; and 101 | 102 | (b) You must cause any modified files to carry prominent notices 103 | stating that You changed the files; and 104 | 105 | (c) You must retain, in the Source form of any Derivative Works 106 | that You distribute, all copyright, patent, trademark, and 107 | attribution notices from the Source form of the Work, 108 | excluding those notices that do not pertain to any part of 109 | the Derivative Works; and 110 | 111 | (d) If the Work includes a "NOTICE" text file as part of its 112 | distribution, then any Derivative Works that You distribute must 113 | include a readable copy of the attribution notices contained 114 | within such NOTICE file, excluding those notices that do not 115 | pertain to any part of the Derivative Works, in at least one 116 | of the following places: within a NOTICE text file distributed 117 | as part of the Derivative Works; within the Source form or 118 | documentation, if provided along with the Derivative Works; or, 119 | within a display generated by the Derivative Works, if and 120 | wherever such third-party notices normally appear. The contents 121 | of the NOTICE file are for informational purposes only and 122 | do not modify the License. You may add Your own attribution 123 | notices within Derivative Works that You distribute, alongside 124 | or as an addendum to the NOTICE text from the Work, provided 125 | that such additional attribution notices cannot be construed 126 | as modifying the License. 127 | 128 | You may add Your own copyright statement to Your modifications and 129 | may provide additional or different license terms and conditions 130 | for use, reproduction, or distribution of Your modifications, or 131 | for any such Derivative Works as a whole, provided Your use, 132 | reproduction, and distribution of the Work otherwise complies with 133 | the conditions stated in this License. 134 | 135 | 5. Submission of Contributions. Unless You explicitly state otherwise, 136 | any Contribution intentionally submitted for inclusion in the Work 137 | by You to the Licensor shall be under the terms and conditions of 138 | this License, without any additional terms or conditions. 139 | Notwithstanding the above, nothing herein shall supersede or modify 140 | the terms of any separate license agreement you may have executed 141 | with Licensor regarding such Contributions. 142 | 143 | 6. Trademarks. This License does not grant permission to use the trade 144 | names, trademarks, service marks, or product names of the Licensor, 145 | except as required for reasonable and customary use in describing the 146 | origin of the Work and reproducing the content of the NOTICE file. 147 | 148 | 7. Disclaimer of Warranty. Unless required by applicable law or 149 | agreed to in writing, Licensor provides the Work (and each 150 | Contributor provides its Contributions) on an "AS IS" BASIS, 151 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 152 | implied, including, without limitation, any warranties or conditions 153 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 154 | PARTICULAR PURPOSE. You are solely responsible for determining the 155 | appropriateness of using or redistributing the Work and assume any 156 | risks associated with Your exercise of permissions under this License. 157 | 158 | 8. Limitation of Liability. In no event and under no legal theory, 159 | whether in tort (including negligence), contract, or otherwise, 160 | unless required by applicable law (such as deliberate and grossly 161 | negligent acts) or agreed to in writing, shall any Contributor be 162 | liable to You for damages, including any direct, indirect, special, 163 | incidental, or consequential damages of any character arising as a 164 | result of this License or out of the use or inability to use the 165 | Work (including but not limited to damages for loss of goodwill, 166 | work stoppage, computer failure or malfunction, or any and all 167 | other commercial damages or losses), even if such Contributor 168 | has been advised of the possibility of such damages. 169 | 170 | 9. Accepting Warranty or Additional Liability. While redistributing 171 | the Work or Derivative Works thereof, You may choose to offer, 172 | and charge a fee for, acceptance of support, warranty, indemnity, 173 | or other liability obligations and/or rights consistent with this 174 | License. However, in accepting such obligations, You may act only 175 | on Your own behalf and on Your sole responsibility, not on behalf 176 | of any other Contributor, and only if You agree to indemnify, 177 | defend, and hold each Contributor harmless for any liability 178 | incurred by, or claims asserted against, such Contributor by reason 179 | of your accepting any such warranty or additional liability. 180 | 181 | END OF TERMS AND CONDITIONS 182 | 183 | APPENDIX: How to apply the Apache License to your work. 184 | 185 | To apply the Apache License to your work, attach the following 186 | boilerplate notice, with the fields enclosed by brackets "[]" 187 | replaced with your own identifying information. (Don't include 188 | the brackets!) The text should be enclosed in the appropriate 189 | comment syntax for the file format. We also recommend that a 190 | file or class name and description of purpose be included on the 191 | same "printed page" as the copyright notice for easier 192 | identification within third-party archives. 193 | 194 | Copyright [yyyy] [name of copyright owner] 195 | 196 | Licensed under the Apache License, Version 2.0 (the "License"); 197 | you may not use this file except in compliance with the License. 198 | You may obtain a copy of the License at 199 | 200 | http://www.apache.org/licenses/LICENSE-2.0 201 | 202 | Unless required by applicable law or agreed to in writing, software 203 | distributed under the License is distributed on an "AS IS" BASIS, 204 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 205 | See the License for the specific language governing permissions and 206 | limitations under the License. 207 | 208 | **m3u8 by Alexander I.Grafov is covered under the BSD-3 license (reproduced below). 209 | 210 | Copyright (c) 2013-2016 Alexander I.Grafov 211 | Copyright (c) 2013-2016 The Project Developers. 212 | 213 | All rights reserved. 214 | 215 | Redistribution and use in source and binary forms, with or without modification, 216 | are permitted provided that the following conditions are met: 217 | 218 | Redistributions of source code must retain the above copyright notice, this 219 | list of conditions and the following disclaimer. 220 | 221 | Redistributions in binary form must reproduce the above copyright notice, this 222 | list of conditions and the following disclaimer in the documentation and/or 223 | other materials provided with the distribution. 224 | 225 | Neither the name of the author nor the names of its 226 | contributors may be used to endorse or promote products derived from 227 | this software without specific prior written permission. 228 | 229 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 230 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 231 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 232 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 233 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 234 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 235 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 236 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 237 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 238 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 239 | 240 | **uuid by Google Inc. is covered under the BSD-3 license (reproduced below). 241 | 242 | Copyright (c) 2009,2014 Google Inc. All rights reserved. 243 | 244 | Redistribution and use in source and binary forms, with or without 245 | modification, are permitted provided that the following conditions are 246 | met: 247 | 248 | * Redistributions of source code must retain the above copyright 249 | notice, this list of conditions and the following disclaimer. 250 | * Redistributions in binary form must reproduce the above 251 | copyright notice, this list of conditions and the following disclaimer 252 | in the documentation and/or other materials provided with the 253 | distribution. 254 | * Neither the name of Google Inc. nor the names of its 255 | contributors may be used to endorse or promote products derived from 256 | this software without specific prior written permission. 257 | 258 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 259 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 260 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 261 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 262 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 263 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 264 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 265 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 266 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 267 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 268 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 269 | 270 | **node-jsonfile by JP Richardson. is covered under the MIT license (reproduced below). 271 | 272 | (The MIT License) 273 | 274 | Copyright (c) 2012-2015, JP Richardson 275 | 276 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 277 | (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, 278 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 279 | furnished to do so, subject to the following conditions: 280 | 281 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 282 | 283 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 284 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 285 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 286 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------------------------------