├── .gitignore
├── .nowignore
├── .prettierrc
├── README.md
├── android-live-streaming
├── .gitignore
├── LICENSE
├── README.md
├── app
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── ic_launcher-playstore.png
│ │ ├── java
│ │ └── com
│ │ │ └── example
│ │ │ └── muxlive
│ │ │ ├── BroadcastActivity.java
│ │ │ └── ConfigureActivity.java
│ │ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ ├── ic_launcher_background.xml
│ │ └── mux.png
│ │ ├── layout
│ │ ├── activity_broadcast.xml
│ │ └── activity_configure.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ ├── values-night
│ │ └── themes.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── strings.xml
│ │ └── themes.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── screenshots
│ ├── animated.gif
│ └── banner.png
└── settings.gradle
├── astro-uploader-and-player
├── .gitignore
├── .vscode
│ ├── extensions.json
│ └── launch.json
├── README.md
├── astro.config.mjs
├── package-lock.json
├── package.json
├── public
│ └── favicon.svg
├── src
│ ├── actions
│ │ └── index.ts
│ ├── env.d.ts
│ ├── layouts
│ │ └── Layout.astro
│ ├── lib
│ │ └── mux.ts
│ └── pages
│ │ ├── index.astro
│ │ ├── playback
│ │ └── [playbackId].astro
│ │ ├── status
│ │ └── [assetId].astro
│ │ └── webhook.json.ts
└── tsconfig.json
├── aws-recommendation-engine
├── .gitignore
├── .npmignore
├── CODEOWNERS
├── README.md
├── bin
│ └── mux-example-aws-recommendations.ts
├── cdk.json
├── jest.config.js
├── lib
│ ├── kinesis-handler-lambda
│ │ ├── VideoView.ts
│ │ ├── event.json
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── video_view.proto
│ │ └── yarn.lock
│ ├── mux-example-aws-recommendations-stack.ts
│ └── recommend-lambda
│ │ ├── event.json
│ │ ├── index.ts
│ │ ├── package.json
│ │ └── yarn.lock
├── package-lock.json
├── package.json
├── test
│ └── mux-example-aws-recommendations.test.ts
└── tsconfig.json
├── contentful-uploader
├── .babelrc
├── .env.example
├── .gitignore
├── README.md
├── extension.json
├── now.json
├── package-lock.json
├── package.json
├── screenshots
│ ├── contentful-appearance.png
│ └── contentful-install-from-github.png
├── src
│ ├── index.css
│ ├── index.html
│ ├── index.tsx
│ ├── player.css
│ └── preview.tsx
└── tsconfig.json
├── gatsby-example-graphql
├── .gitignore
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── env.example
├── gatsby-browser.js
├── gatsby-config.js
├── gatsby-node.js
├── package.json
├── src
│ ├── components
│ │ ├── video-player.js
│ │ └── video-player.module.css
│ ├── styles
│ │ └── global.css
│ └── templates
│ │ └── index.js
├── static
│ └── favicon.ico
└── yarn.lock
├── ios-live-streaming
├── .gitignore
├── LICENSE
├── MuxLive.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── MuxLive.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── MuxLive
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── Icon-App-20x20@2x-1.png
│ │ │ ├── Icon-App-20x20@2x-2.png
│ │ │ ├── Icon-App-20x20@2x.png
│ │ │ ├── Icon-App-20x20@3x.png
│ │ │ ├── Icon-App-29x29@2x.png
│ │ │ ├── Icon-App-29x29@3x.png
│ │ │ ├── Icon-App-40x40@2x.png
│ │ │ ├── Icon-App-40x40@3x.png
│ │ │ ├── Icon-App-60x60@2x.png
│ │ │ ├── Icon-App-60x60@3x.png
│ │ │ ├── Icon-App-iTunes.png
│ │ │ ├── icon-152.png
│ │ │ ├── icon-167.png
│ │ │ ├── icon-20.png
│ │ │ ├── icon-29.png
│ │ │ ├── icon-58.png
│ │ │ ├── icon-76.png
│ │ │ └── icon-80.png
│ │ ├── Contents.json
│ │ ├── mux-logo.imageset
│ │ │ ├── Contents.json
│ │ │ └── mux-logo.png
│ │ └── small-mux-logo.imageset
│ │ │ ├── Contents.json
│ │ │ └── small-mux-logo.png
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── BroadcastViewController.swift
│ ├── ConnectViewController.swift
│ ├── Info.plist
│ ├── MuxButton.swift
│ ├── MuxTextField.swift
│ └── SceneDelegate.swift
├── Podfile
├── Podfile.lock
├── README.md
└── screenshots
│ ├── animated.gif
│ └── banner.png
├── mediarecorder-streaming-uploads
├── README.md
├── app.js
└── index.html
├── nextjs-mdx-player
├── .eslintrc.json
├── .gitignore
├── README.md
├── blog
│ └── awesome-blog-post.mdx
├── next.config.js
├── package-lock.json
├── package.json
├── pages
│ ├── _app.js
│ ├── index.js
│ └── posts
│ │ └── [slug].js
├── public
│ ├── favicon.ico
│ └── vercel.svg
├── styles
│ ├── Home.module.css
│ └── globals.css
└── utils
│ └── blogPosts.js
├── nextjs-uploader-and-player
└── README.md
├── now-airtable
├── .env.example
├── README.md
├── api
│ ├── add-video.js
│ ├── get-video.js
│ ├── list-videos.js
│ └── mux-callback.js
├── components
│ ├── layout.js
│ ├── player.js
│ └── thumbnail.js
├── next.config.js
├── now.json
├── package.json
├── pages
│ ├── index.js
│ ├── show.js
│ └── upload.js
├── utils
│ ├── airtable.js
│ ├── config.js
│ ├── decorate.js
│ └── theme.js
└── yarn.lock
├── remixjs-uploader-and-player
└── README.md
├── signed-playback-lambda
├── .gitignore
├── README.md
├── lambda.js
├── package.json
├── webpack.config.js
└── yarn.lock
├── signed-playback-netlify
├── .gitignore
├── README.md
├── functions
│ └── sign_playback_id.js
├── netlify.toml
├── package.json
├── src
│ └── mux_signatures.js
└── yarn.lock
├── sveltekit-uploader-and-player
├── .env.example
├── .eslintignore
├── .eslintrc.cjs
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── README.md
├── package-lock.json
├── package.json
├── postcss.config.cjs
├── src
│ ├── app.d.ts
│ ├── app.html
│ ├── app.pcss
│ ├── lib
│ │ └── mux.ts
│ └── routes
│ │ ├── +layout.svelte
│ │ ├── +page.server.ts
│ │ ├── +page.svelte
│ │ ├── api
│ │ └── mux
│ │ │ └── webhook
│ │ │ └── +server.ts
│ │ ├── playback
│ │ └── [playbackId]
│ │ │ └── +page.svelte
│ │ └── status
│ │ └── [assetId]
│ │ ├── +page.server.ts
│ │ └── +page.svelte
├── static
│ └── favicon.png
├── svelte.config.js
├── tailwind.config.cjs
├── tsconfig.json
└── vite.config.ts
├── swift-data-library-installation
├── MuxDataContainer
│ ├── .swiftpm
│ │ └── xcode
│ │ │ └── package.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── Package.swift
│ ├── Sources
│ │ └── MuxDataContainer
│ │ │ ├── MonitoringContainer.swift
│ │ │ └── ProcessInfo+Mux.swift
│ └── Tests
│ │ └── MuxDataContainerTests
│ │ └── MuxDataContainerTests.swift
├── README.md
└── SwiftDataLibraryInstallation
│ ├── SwiftDataLibraryInstallation.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── SwiftDataLibraryInstallation.xcscheme
│ ├── SwiftDataLibraryInstallation
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── BasicPlaybackExampleViewController.swift
│ ├── Info.plist
│ ├── PlayerLayerExampleViewController.swift
│ └── SceneDelegate.swift
│ ├── SwiftDataLibraryInstallationTests
│ └── SwiftDataLibraryInstallationTests.swift
│ └── SwiftDataLibraryInstallationUITests
│ ├── SwiftDataLibraryInstallationUITests.swift
│ └── SwiftDataLibraryInstallationUITestsLaunchTests.swift
├── swift-video-app
├── MuxExampleVideoApp
│ ├── MuxExampleVideoApp.xcodeproj
│ │ ├── project.pbxproj
│ │ ├── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ ├── xcshareddata
│ │ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ │ └── swiftpm
│ │ │ │ │ └── Package.resolved
│ │ │ └── xcuserdata
│ │ │ │ └── dylanjhaveri.xcuserdatad
│ │ │ │ └── UserInterfaceState.xcuserstate
│ │ ├── xcshareddata
│ │ │ └── xcschemes
│ │ │ │ └── MuxExampleVideoApp.xcscheme
│ │ └── xcuserdata
│ │ │ └── dylanjhaveri.xcuserdatad
│ │ │ └── xcschemes
│ │ │ └── xcschememanagement.plist
│ ├── MuxExampleVideoApp
│ │ ├── .gitignore
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets
│ │ │ ├── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ ├── Base.lproj
│ │ │ ├── LaunchScreen.storyboard
│ │ │ └── Main.storyboard
│ │ ├── BasicPlaybackExampleViewController.swift
│ │ ├── Info.plist
│ │ ├── PlayerLayerExampleViewController.swift
│ │ └── SceneDelegate.swift
│ ├── MuxExampleVideoAppTests
│ │ ├── Info.plist
│ │ └── MuxExampleVideoAppTests.swift
│ └── MuxExampleVideoAppUITests
│ │ ├── Info.plist
│ │ └── MuxExampleVideoAppUITests.swift
└── README.md
└── webhook-notifications-knock
├── .env.example
├── .eslintrc.json
├── .gitignore
├── README.md
├── app
├── api
│ ├── knock
│ │ ├── channels
│ │ │ └── route.ts
│ │ ├── on-play
│ │ │ └── route.ts
│ │ └── subscriptions
│ │ │ └── route.ts
│ └── webhooks
│ │ └── mux
│ │ ├── route.ts
│ │ ├── video.asset.ready.json
│ │ └── video.live_stream.active.json
├── components
│ ├── ApiTriggers.tsx
│ ├── FeedContainer.tsx
│ ├── FeedToasts.tsx
│ ├── SubscribeButton.tsx
│ ├── SubscriberCount.tsx
│ └── player.tsx
├── favicon.ico
├── globals.css
├── layout.tsx
├── lib
│ └── knock.ts
├── page.tsx
├── trigger-apis
│ └── page.tsx
└── users
│ └── [userId]
│ └── page.tsx
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── next.svg
├── placeholder-user.jpg
├── placeholder.svg
└── vercel.svg
├── tailwind.config.ts
└── tsconfig.json
/.nowignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .cache
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "singleQuote": true
5 | }
6 |
--------------------------------------------------------------------------------
/android-live-streaming/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Built application files
3 | *.apk
4 | *.aar
5 | *.ap_
6 | *.aab
7 |
8 | # Files for the ART/Dalvik VM
9 | *.dex
10 |
11 | # Java class files
12 | *.class
13 |
14 | # Generated files
15 | bin/
16 | gen/
17 | out/
18 | # Uncomment the following line in case you need and you don't have the release build type files in your app
19 | # release/
20 |
21 | # Gradle files
22 | .gradle/
23 | build/
24 |
25 | # Local configuration file (sdk path, etc)
26 | local.properties
27 |
28 | # Proguard folder generated by Eclipse
29 | proguard/
30 |
31 | # Log Files
32 | *.log
33 |
34 | # Android Studio Navigation editor temp files
35 | .navigation/
36 |
37 | # Android Studio captures folder
38 | captures/
39 |
40 | # IntelliJ
41 | .idea/
42 | *.iml
43 | .idea/workspace.xml
44 | .idea/tasks.xml
45 | .idea/gradle.xml
46 | .idea/assetWizardSettings.xml
47 | .idea/dictionaries
48 | .idea/libraries
49 | # Android Studio 3 in .gitignore file.
50 | .idea/caches
51 | .idea/modules.xml
52 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
53 | .idea/navEditor.xml
54 |
55 | # Keystore files
56 | # Uncomment the following lines if you do not want to check your keystore files in.
57 | #*.jks
58 | #*.keystore
59 |
60 | # External native build folder generated in Android Studio 2.2 and later
61 | .externalNativeBuild
62 | .cxx/
63 |
64 | # Google Services (e.g. APIs or Firebase)
65 | # google-services.json
66 |
67 | # Freeline
68 | freeline.py
69 | freeline/
70 | freeline_project_description.json
71 |
72 | # fastlane
73 | fastlane/report.xml
74 | fastlane/Preview.html
75 | fastlane/screenshots
76 | fastlane/test_output
77 | fastlane/readme.md
78 |
79 | # Version control
80 | vcs.xml
81 |
82 | # lint
83 | lint/intermediates/
84 | lint/generated/
85 | lint/outputs/
86 | lint/tmp/
87 | # lint/reports/
88 |
89 | # Android Profiling
90 | *.hprof
91 |
92 |
93 | **/.idea/workspace.xml
94 | **/.idea/tasks.xml
95 |
96 | .DS_Store
97 | *~
98 |
--------------------------------------------------------------------------------
/android-live-streaming/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Mux, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/android-live-streaming/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/android-live-streaming/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | }
4 |
5 | android {
6 | compileSdkVersion 30
7 | buildToolsVersion "29.0.3"
8 |
9 | defaultConfig {
10 | applicationId "com.example.muxlive"
11 | minSdkVersion 21
12 | targetSdkVersion 30
13 | versionCode 1
14 | versionName "1.0"
15 |
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 | compileOptions {
26 | sourceCompatibility JavaVersion.VERSION_1_8
27 | targetCompatibility JavaVersion.VERSION_1_8
28 | }
29 | }
30 |
31 | dependencies {
32 |
33 | implementation 'androidx.appcompat:appcompat:1.1.0'
34 | implementation 'com.google.android.material:material:1.1.0'
35 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
36 | implementation 'com.github.pedroSG94.rtmp-rtsp-stream-client-java:rtplibrary:1.9.5'
37 | testImplementation 'junit:junit:4.+'
38 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
39 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
40 | }
41 |
--------------------------------------------------------------------------------
/android-live-streaming/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
19 |
20 |
27 |
30 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/drawable/mux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/app/src/main/res/drawable/mux.png
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 | #FB2490
11 | #AB1B64
12 |
13 |
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #211F1F
4 |
5 |
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Mux Live
3 |
4 |
--------------------------------------------------------------------------------
/android-live-streaming/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
--------------------------------------------------------------------------------
/android-live-streaming/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | repositories {
4 | google()
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath "com.android.tools.build:gradle:4.1.1"
9 |
10 | // NOTE: Do not place your application dependencies here; they belong
11 | // in the individual module build.gradle files
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | google()
18 | jcenter()
19 | maven { url 'https://jitpack.io' }
20 | }
21 | }
22 |
23 | task clean(type: Delete) {
24 | delete rootProject.buildDir
25 | }
26 |
--------------------------------------------------------------------------------
/android-live-streaming/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 |
--------------------------------------------------------------------------------
/android-live-streaming/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/android-live-streaming/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Dec 03 11:36:30 GMT 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
7 |
--------------------------------------------------------------------------------
/android-live-streaming/screenshots/animated.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/screenshots/animated.gif
--------------------------------------------------------------------------------
/android-live-streaming/screenshots/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/android-live-streaming/screenshots/banner.png
--------------------------------------------------------------------------------
/android-live-streaming/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | rootProject.name = "MuxLive"
3 |
--------------------------------------------------------------------------------
/astro-uploader-and-player/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 |
4 | # generated types
5 | .astro/
6 |
7 | # dependencies
8 | node_modules/
9 |
10 | # logs
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # environment variables
17 | .env
18 | .env.production
19 |
20 | # macOS-specific files
21 | .DS_Store
22 |
23 | # jetbrains setting folder
24 | .idea/
25 |
--------------------------------------------------------------------------------
/astro-uploader-and-player/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/astro-uploader-and-player/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/astro-uploader-and-player/astro.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'astro/config';
2 |
3 | // https://astro.build/config
4 | export default defineConfig({
5 | output: 'server',
6 | experimental: {
7 | actions: true,
8 | },
9 | });
10 |
--------------------------------------------------------------------------------
/astro-uploader-and-player/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "astro-uploader-and-player",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "scripts": {
6 | "dev": "astro dev",
7 | "start": "astro dev",
8 | "build": "astro check && astro build",
9 | "preview": "astro preview",
10 | "astro": "astro"
11 | },
12 | "dependencies": {
13 | "@astrojs/check": "^0.7.0",
14 | "@mux/mux-node": "^8.7.1",
15 | "@mux/mux-player": "^2.7.0",
16 | "@mux/mux-uploader": "^1.0.0-beta.18",
17 | "astro": "^4.10.0",
18 | "typescript": "^5.4.5"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/astro-uploader-and-player/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
--------------------------------------------------------------------------------
/astro-uploader-and-player/src/actions/index.ts:
--------------------------------------------------------------------------------
1 | import { defineAction, z } from 'astro:actions';
2 | import mux from '../lib/mux';
3 |
4 | export const server = {
5 | getAssetForUpload: defineAction({
6 | accept: 'form',
7 | input: z.object({
8 | uploadId: z.string(),
9 | }),
10 | handler: async ({ uploadId }) => {
11 | // when the upload is complete,
12 | // the upload will have an assetId associated with it
13 | // we'll use that assetId to view the video status
14 | const upload = await mux.video.uploads.retrieve(uploadId);
15 | console.log({ uploadId, upload });
16 | if (upload.asset_id) {
17 | return { assetId: upload.asset_id };
18 | }
19 |
20 | // while onSuccess is a strong indicator that Mux has received the file
21 | // and created the asset, this isn't a guarantee.
22 | // In production, you might write an api route
23 | // to listen for the`video.upload.asset_created` webhook
24 | // https://docs.mux.com/guides/listen-for-webhooks
25 | // However, to keep things simple here,
26 | // we'll just ask the user to push the button again.
27 | // This should rarely happen.
28 | return { message: 'Upload has no asset yet. Try again.' };
29 | },
30 | }),
31 | };
32 |
--------------------------------------------------------------------------------
/astro-uploader-and-player/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/astro-uploader-and-player/src/layouts/Layout.astro:
--------------------------------------------------------------------------------
1 | ---
2 | interface Props {
3 | title: string;
4 | }
5 |
6 | const { title } = Astro.props;
7 | ---
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {title}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/astro-uploader-and-player/src/lib/mux.ts:
--------------------------------------------------------------------------------
1 | import Mux from '@mux/mux-node';
2 |
3 | const mux = new Mux({
4 | tokenId: import.meta.env.MUX_TOKEN_ID,
5 | tokenSecret: import.meta.env.MUX_TOKEN_SECRET,
6 | });
7 |
8 | export default mux;
9 |
--------------------------------------------------------------------------------
/astro-uploader-and-player/src/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Layout from '../layouts/Layout.astro';
3 | import mux from '../lib/mux';
4 |
5 | const upload = await mux.video.uploads.create({
6 | new_asset_settings: {
7 | playback_policy: ['public'],
8 | encoding_tier: 'baseline',
9 | },
10 | cors_origin: '*',
11 | });
12 | ---
13 |
14 |
15 |
16 |
17 |
21 |
50 |
51 |
--------------------------------------------------------------------------------
/astro-uploader-and-player/src/pages/playback/[playbackId].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Layout from '../../layouts/Layout.astro';
3 |
4 | const { playbackId } = Astro.params;
5 |
6 | const title = 'View this video created with Mux + Astro';
7 | const description =
8 | 'This video was uploaded and processed by Mux in an example Remix application.';
9 | ---
10 |
11 |
12 |
13 |
14 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
30 |
31 |
32 |
33 |
34 |
35 |
39 |
40 |
41 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/astro-uploader-and-player/src/pages/status/[assetId].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Layout from '../../layouts/Layout.astro';
3 | import mux from '../../lib/mux';
4 |
5 | const { assetId } = Astro.params;
6 |
7 | // now that we have an assetId, we can see how the video is doing.
8 | // in production, you might have some setup where a Mux webhook
9 | // tells your server the status of your asset.
10 | // https://docs.mux.com/guides/listen-for-webhooks
11 | // for this example, however, we'll just ask the Mux API ourselves
12 | if (typeof assetId !== 'string') {
13 | return new Response(null, {
14 | status: 404,
15 | statusText: 'Not found',
16 | });
17 | }
18 | const asset = await mux.video.assets.retrieve(assetId);
19 |
20 | // if the asset is ready and it has a public playback ID,
21 | // (which it should, considering the upload settings we used)
22 | // redirect to its playback page
23 | if (asset.status === 'ready') {
24 | const playbackIds = asset.playback_ids;
25 | if (Array.isArray(playbackIds)) {
26 | const playbackId = playbackIds.find((id) => id.policy === 'public');
27 | if (playbackId) {
28 | return Astro.redirect(`/playback/${playbackId.id}`);
29 | }
30 | }
31 | }
32 | ---
33 |
34 |
35 | {
36 | // in most cases, the asset is just preparing.
37 | // Let's say something to that effect.
38 | asset.status === 'preparing' ? (
39 | Asset is preparing...
40 | ) : (
41 | // if not "preparing", then "errored" or "ready"
42 | // if "errored", we'll show the errors
43 | // we don't expect to see "ready" because "ready" should redirect in the loader
44 |
45 |
46 | Asset is in an unexpected state: {status}
.
47 |
48 | {Array.isArray(asset.errors) ? (
49 |
50 | {asset.errors.map((error, key) => (
51 | {JSON.stringify(error)}
52 | ))}
53 |
54 | ) : null}
55 |
56 | This is awkward. Let's refresh and try again.
57 |
58 |
59 | )
60 | }
61 |
62 |
63 |
68 |
69 |
--------------------------------------------------------------------------------
/astro-uploader-and-player/src/pages/webhook.json.ts:
--------------------------------------------------------------------------------
1 | import type { APIRoute } from 'astro';
2 | import mux from '../lib/mux';
3 |
4 | // while this isn't called anywhere in this example,
5 | // I thought it might be helpful to see what a mux webhook handler looks like.
6 |
7 | export const POST: APIRoute = async ({ request }) => {
8 | const body = await request.text();
9 | // mux.webhooks.unwrap will validate that the given payload was sent by Mux and parse the payload.
10 | // It will also provide type-safe access to the payload.
11 | // Generate MUX_WEBHOOK_SIGNING_SECRET in the Mux dashboard
12 | // https://dashboard.mux.com/settings/webhooks
13 | const event = mux.webhooks.unwrap(
14 | body,
15 | request.headers,
16 | process.env.MUX_WEBHOOK_SIGNING_SECRET
17 | );
18 |
19 | // you can also unwrap the payload yourself:
20 | // const event = await request.json();
21 | switch (event.type) {
22 | case 'video.upload.asset_created':
23 | // we might use this to know that an upload has been completed
24 | // and we can save its assetId to our database
25 | break;
26 | case 'video.asset.ready':
27 | // we might use this to know that a video has been encoded
28 | // and we can save its playbackId to our database
29 | break;
30 | // there are many more Mux webhook events
31 | // check them out at https://docs.mux.com/webhook-reference
32 | default:
33 | break;
34 | }
35 |
36 | return new Response(JSON.stringify({ message: 'ok' }), {
37 | headers: {
38 | 'Content-Type': 'application/json',
39 | },
40 | });
41 | };
42 |
--------------------------------------------------------------------------------
/astro-uploader-and-player/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict"
3 | }
--------------------------------------------------------------------------------
/aws-recommendation-engine/.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 |
--------------------------------------------------------------------------------
/aws-recommendation-engine/.npmignore:
--------------------------------------------------------------------------------
1 | *.ts
2 | !*.d.ts
3 |
4 | # CDK asset staging directory
5 | .cdk.staging
6 | cdk.out
7 |
--------------------------------------------------------------------------------
/aws-recommendation-engine/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @muxinc/community-engineering
2 | /CODEOWNERS @muxinc/platform-engineering
3 |
--------------------------------------------------------------------------------
/aws-recommendation-engine/README.md:
--------------------------------------------------------------------------------
1 | # Mux AWS recommendation engine example
2 |
3 | This repository contains an example implementation of how you can set up Mux streaming exports and Amazon Personalize to build your own content recommendation engine.
4 |
5 | For a detailed walkthrough on how you can use this repository, check out the [corollary blog post](https://mux.com/blog/amazon-personalize-video-recommendation-engine) over on the Mux blog.
6 | ## Useful commands
7 |
8 | * `npm run build` compile typescript to js
9 | * `npm run watch` watch for changes and compile
10 | * `npm run test` perform the jest unit tests
11 | * `cdk deploy` deploy this stack to your default AWS account/region
12 | * `cdk diff` compare deployed stack with current state
13 | * `cdk synth` emits the synthesized CloudFormation template
14 | ## References and resources
15 | * https://docs.aws.amazon.com/general/latest/gr/personalize.html
16 | * https://github.com/aws-samples/amazon-personalize-ingestion-pipeline
17 | * https://github.com/CloudedThings/100-Days-in-Cloud/blob/main/Labs/80-Amazon-Rekognition-CDK-deployed/index.py
18 | * https://github.com/aws-samples/amazon-rekognition-large-scale-processing
19 | * https://docs.aws.amazon.com/personalize/latest/dg/recording-events.html#event-record-api
20 | * https://docs.aws.amazon.com/personalize/latest/dg/API_UBS_PutItems.html
21 | * https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-personalize-events/classes/putitemscommand.html
22 |
--------------------------------------------------------------------------------
/aws-recommendation-engine/bin/mux-example-aws-recommendations.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import 'source-map-support/register';
3 | import * as cdk from 'aws-cdk-lib';
4 | import { MuxExampleAwsRecommendationsStack } from '../lib/mux-example-aws-recommendations-stack';
5 |
6 | const app = new cdk.App();
7 | new MuxExampleAwsRecommendationsStack(app, 'MuxExampleAwsRecommendationsStack', {
8 | /* If you don't specify 'env', this stack will be environment-agnostic.
9 | * Account/Region-dependent features and context lookups will not work,
10 | * but a single synthesized template can be deployed anywhere. */
11 |
12 | /* Uncomment the next line to specialize this stack for the AWS Account
13 | * and Region that are implied by the current CLI configuration. */
14 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
15 |
16 | /* Uncomment the next line if you know exactly what Account and Region you
17 | * want to deploy the stack to. */
18 | // env: { account: '123456789012', region: 'us-east-1' },
19 |
20 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
21 | });
--------------------------------------------------------------------------------
/aws-recommendation-engine/cdk.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": "npx ts-node --prefer-ts-exts bin/mux-example-aws-recommendations.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-apigateway:usagePlanKeyOrderInsensitiveId": true,
21 | "@aws-cdk/core:stackRelativeExports": true,
22 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
23 | "@aws-cdk/aws-lambda:recognizeVersionProps": true,
24 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
25 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true,
26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
28 | "@aws-cdk/core:checkSecretUsage": true,
29 | "@aws-cdk/aws-iam:minimizePolicies": true,
30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
32 | "@aws-cdk/core:target-partitions": [
33 | "aws",
34 | "aws-cn"
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/aws-recommendation-engine/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'node',
3 | roots: ['/test'],
4 | testMatch: ['**/*.test.ts'],
5 | transform: {
6 | '^.+\\.tsx?$': 'ts-jest'
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/aws-recommendation-engine/lib/kinesis-handler-lambda/event.json:
--------------------------------------------------------------------------------
1 | {
2 | "Records": [{
3 | "Sns": {
4 | "Type": "Notification",
5 | MessageId: '88daf6f7-3f55-5fda-85dc-adc9596a88ba',
6 | TopicArn: 'arn:aws:sns:us-east-1:618963333842:AmazonRekognitionMuxVideo',
7 | Subject: null,
8 | Message: '{"JobId":"9864ee6a6d33bc4c3a195ab868720ea532567617e5049631d16e93478411da61","Status":"SUCCEEDED","API":"StartLabelDetection","JobTag":"string","Timestamp":1657037823845,"Video":{"S3ObjectName":"hackweek-mux-video-ad-final.mp4","S3Bucket":"dk-videos-bucket"}}',
9 | Timestamp: '2022-07-05T16:17:03.978Z',
10 | SignatureVersion: '1',
11 | Signature: 'LXrafz0da/y67FFTR323zUDxWSKtut9evVgolpI1KhtyG8nsWxlMEcaCACDve//0Ar9SDMcTnSdUt8RckwheYjdEIJpUb9SsmEWqlkU77qN5vDBk5rP/Bf92cBxiranSZLx9Y3cZjQAvSSUvbVz0VWJRn8U6TXd9yE0CsQQG+Qm6hJQNpVm740o2XxG6oIynd+JWBdS342/vmsQypnFy0uI3UD4lOknqKKF8TgzQN9xNLM/IOdln+oHBx3XWESCLJI1PLHcuG3CV1xKGq1+UaGIIHJa+JpnhBHtJNwxGKR8CBvYeogbhZVBbAYXrSXWUUbp9RNT6kT/HDbhAhOL1lw==',
12 | SigningCertUrl: 'https://sns.us-east-1.amazonaws.com/SimpleNotificationService-7ff5318490ec183fbaddaa2a969abfda.pem',
13 | UnsubscribeUrl: 'https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:618963333842:AmazonRekognitionMuxVideo:ff670b88-c438-4ecb-a3ad-543dd6fd4979',
14 | MessageAttributes: {}
15 | }
16 | }]
17 | }
--------------------------------------------------------------------------------
/aws-recommendation-engine/lib/kinesis-handler-lambda/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lambda",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "dependencies": {
7 | "@aws-sdk/client-personalize-events": "^3.121.0",
8 | "@aws-sdk/client-rekognition": "^3.118.1",
9 | "@mux/mux-node": "^5.1.0",
10 | "protobufjs": "^7.0.0",
11 | "typescript": "^4.7.4"
12 | },
13 | "devDependencies": {
14 | "@types/aws-lambda": "^8.10.101"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/aws-recommendation-engine/lib/recommend-lambda/event.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/aws-recommendation-engine/lib/recommend-lambda/index.ts:
--------------------------------------------------------------------------------
1 | import { PersonalizeRuntimeClient, GetRecommendationsCommand, PredictedItem } from "@aws-sdk/client-personalize-runtime";
2 |
3 | import { APIGatewayEvent, Context } from "aws-lambda";
4 | const personalizeClient = new PersonalizeRuntimeClient({ region: process.env.AWS_REGION });
5 |
6 | const MOST_POPULAR_RECOMMENDER = "arn:aws:personalize:::recipe/aws-vod-most-popular";
7 | const BECAUSE_YOU_WATCHED_X_RECOMMENDER = "arn:aws:personalize:::recipe/aws-vod-because-you-watched-x";
8 | const TOP_PICKS_FOR_YOU_RECOMMENDER = "arn:aws:personalize:::recipe/aws-vod-top-picks";
9 |
10 | exports.handler = async function (event: APIGatewayEvent, context: Context): Promise {
11 | try {
12 |
13 | const userId = event.queryStringParameters?.viewerUserId || "anonymous"
14 | const assetId = event.queryStringParameters?.assetId || "anonymous"
15 |
16 | const response = await personalizeClient.send(
17 | new GetRecommendationsCommand({
18 | userId,
19 | itemId: assetId,
20 | recommenderArn: MOST_POPULAR_RECOMMENDER, // change this out with the ARN representing your desired recommendation strategy
21 | context: {}
22 | })
23 | );
24 |
25 | return response.itemList || [];
26 |
27 | // process data.
28 | } catch (error) {
29 | console.log(JSON.stringify(error, null, 2));
30 | // error handling.
31 | }
32 |
33 | return [];
34 | }
35 |
--------------------------------------------------------------------------------
/aws-recommendation-engine/lib/recommend-lambda/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lambda",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "dependencies": {
7 | "@aws-sdk/client-personalize-events": "^3.121.0",
8 | "@aws-sdk/client-personalize-runtime": "^3.145.0",
9 | "@aws-sdk/client-rekognition": "^3.118.1",
10 | "@mux/mux-node": "^5.1.0",
11 | "protobufjs": "^7.0.0",
12 | "typescript": "^4.7.4"
13 | },
14 | "devDependencies": {
15 | "@types/aws-lambda": "^8.10.101"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/aws-recommendation-engine/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mux-example-aws-recommendations",
3 | "version": "0.1.0",
4 | "bin": {
5 | "mux-example-aws-recommendations": "bin/mux-example-aws-recommendations.js"
6 | },
7 | "scripts": {
8 | "build": "tsc",
9 | "watch": "tsc -w",
10 | "test": "jest",
11 | "cdk": "cdk"
12 | },
13 | "devDependencies": {
14 | "@types/jest": "^27.5.2",
15 | "@types/node": "^10.17.27",
16 | "@types/prettier": "2.6.0",
17 | "aws-cdk": "2.29.1",
18 | "esbuild": "^0.14.48",
19 | "jest": "^27.5.1",
20 | "ts-jest": "^27.1.4",
21 | "ts-node": "^10.8.1",
22 | "typescript": "~3.9.7"
23 | },
24 | "dependencies": {
25 | "aws-cdk-lib": "2.29.1",
26 | "constructs": "^10.0.0",
27 | "source-map-support": "^0.5.21"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/aws-recommendation-engine/test/mux-example-aws-recommendations.test.ts:
--------------------------------------------------------------------------------
1 | // import * as cdk from 'aws-cdk-lib';
2 | // import { Template } from 'aws-cdk-lib/assertions';
3 | // import * as MuxExampleAwsRecommendations from '../lib/mux-example-aws-recommendations-stack';
4 |
5 | // example test. To run these tests, uncomment this file along with the
6 | // example resource in lib/mux-example-aws-recommendations-stack.ts
7 | test('SQS Queue Created', () => {
8 | // const app = new cdk.App();
9 | // // WHEN
10 | // const stack = new MuxExampleAwsRecommendations.MuxExampleAwsRecommendationsStack(app, 'MyTestStack');
11 | // // THEN
12 | // const template = Template.fromStack(stack);
13 |
14 | // template.hasResourceProperties('AWS::SQS::Queue', {
15 | // VisibilityTimeout: 300
16 | // });
17 | });
18 |
--------------------------------------------------------------------------------
/aws-recommendation-engine/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2018",
4 | "module": "commonjs",
5 | "lib": [
6 | "es2018"
7 | ],
8 | "declaration": true,
9 | "strict": true,
10 | "noImplicitAny": true,
11 | "strictNullChecks": true,
12 | "noImplicitThis": true,
13 | "alwaysStrict": true,
14 | "noUnusedLocals": false,
15 | "noUnusedParameters": false,
16 | "noImplicitReturns": true,
17 | "noFallthroughCasesInSwitch": false,
18 | "inlineSourceMap": true,
19 | "inlineSources": true,
20 | "experimentalDecorators": true,
21 | "strictPropertyInitialization": false,
22 | "typeRoots": [
23 | "./node_modules/@types"
24 | ]
25 | },
26 | "exclude": [
27 | "node_modules",
28 | "cdk.out"
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/contentful-uploader/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "useBuiltIns": false,
7 | "modules": false
8 | }
9 | ],
10 | [
11 | "@babel/preset-react",
12 | {
13 | "useBuiltIns": true
14 | }
15 | ]
16 | ],
17 | "plugins": [
18 | [
19 | "@babel/plugin-proposal-class-properties",
20 | {
21 | "loose": true
22 | }
23 | ],
24 | [
25 | "@babel/plugin-transform-runtime",
26 | {
27 | "corejs": false,
28 | "helpers": false,
29 | "regenerator": true
30 | }
31 | ]
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/contentful-uploader/.env.example:
--------------------------------------------------------------------------------
1 | MUX_TOKEN_ID=your-access-token-id
2 | MUX_TOKEN_SECRET=your-access-token-secret
3 |
--------------------------------------------------------------------------------
/contentful-uploader/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # dotenv environment variables file
9 | .env
10 | .contentfulrc.json
11 |
12 | # Parcel-bundler cache
13 | .cache
14 |
15 | # Dependency directories
16 | node_modules/
17 |
18 | # Build
19 | build/
20 |
--------------------------------------------------------------------------------
/contentful-uploader/extension.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "mux-contentful-uploader",
3 | "name": "Mux Contentful Uploader",
4 | "src": "https://contentful.mux.dev/extension",
5 | "fieldTypes": ["Object"],
6 | "parameters": {
7 | "installation": [
8 | {
9 | "id": "muxAccessTokenId",
10 | "name": "Mux Access Token ID",
11 | "description": "You can create access tokens here: https://dashboard.mux.com/settings/access-tokens",
12 | "type": "Symbol",
13 | "required": true
14 | },
15 | {
16 | "id": "muxAccessTokenSecret",
17 | "name": "Mux Access Token Secret",
18 | "description": "You can create access tokens here: https://dashboard.mux.com/settings/access-tokens",
19 | "type": "Symbol",
20 | "required": true
21 | }
22 | ]
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/contentful-uploader/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "alias": "contentful.mux.dev",
4 | "builds": [
5 | {
6 | "src": "screenshots/*.png",
7 | "use": "@now/static"
8 | },
9 | {
10 | "src": "*.md",
11 | "use": "@now/md",
12 | "config": {
13 | "title": "Contentful Mux Video Plugin",
14 | "language": "en",
15 | "meta": [
16 | {
17 | "name": "description",
18 | "content": "Add beautiful video streaming to your Contentful dashboard! Mux Video extension that's installed in minutes"
19 | }
20 | ],
21 | "css": ["https://static.mux.com/css/mux-markdown.css"]
22 | }
23 | },
24 | {
25 | "src": "package.json",
26 | "use": "@now/static-build",
27 | "config": { "distDir": "build" }
28 | }
29 | ],
30 | "routes": [
31 | { "src": "/", "dest": "/README.html" },
32 | { "src": "/extension", "dest": "index.html" }
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/contentful-uploader/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mux-contentful-uploader",
3 | "version": "0.1.0",
4 | "private": true,
5 | "devDependencies": {
6 | "@babel/core": "7.3.4",
7 | "@babel/plugin-proposal-class-properties": "7.3.4",
8 | "@babel/plugin-transform-runtime": "7.3.4",
9 | "@babel/preset-env": "7.3.4",
10 | "@babel/preset-react": "7.0.0",
11 | "@contentful/contentful-extension-scripts": "0.7.1",
12 | "@types/react": "^16.8.8",
13 | "@types/react-dom": "^16.8.3",
14 | "@types/webpack-env": "1.13.9",
15 | "contentful-cli": "0.26.1",
16 | "cssnano": "4.1.10",
17 | "typescript": "3.3.4000"
18 | },
19 | "dependencies": {
20 | "@contentful/forma-36-fcss": "^0.0.16",
21 | "@contentful/forma-36-react-components": "^2.10.3",
22 | "@contentful/forma-36-tokens": "^0.2.3",
23 | "@mux/upchunk": "^1.0.6",
24 | "@types/hls.js": "^0.12.3",
25 | "contentful-ui-extensions-sdk": "3.7.2",
26 | "hls.js": "^0.12.4",
27 | "prop-types": "^15.7.2",
28 | "react": "^16.8.5",
29 | "react-dom": "^16.8.5"
30 | },
31 | "scripts": {
32 | "prestart": "contentful extension update --src http://localhost:1234 --force",
33 | "start": "contentful-extension-scripts start",
34 | "build": "contentful-extension-scripts build",
35 | "deploy": "npm run build && now",
36 | "configure": "contentful space use && contentful space environment use",
37 | "login": "contentful login",
38 | "logout": "contentful logout",
39 | "help": "contentful-extension-scripts help",
40 | "now-build": "npm run build",
41 | "install-extension": "eval `cat .env` && contentful extension create --installation-parameters '{\"muxAccessTokenId\": \"'$MUX_TOKEN_ID'\", \"muxAccessTokenSecret\": \"'$MUX_TOKEN_SECRET'\"}'"
42 | },
43 | "browserslist": [
44 | "last 5 Chrome version",
45 | "> 1%",
46 | "not ie <= 11"
47 | ]
48 | }
49 |
--------------------------------------------------------------------------------
/contentful-uploader/screenshots/contentful-appearance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/contentful-uploader/screenshots/contentful-appearance.png
--------------------------------------------------------------------------------
/contentful-uploader/screenshots/contentful-install-from-github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/contentful-uploader/screenshots/contentful-install-from-github.png
--------------------------------------------------------------------------------
/contentful-uploader/src/index.css:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | div {
4 | margin: 0;
5 | padding: 0;
6 | border: 0;
7 | font-size: 100%;
8 | font: inherit;
9 | vertical-align: baseline;
10 | }
11 |
12 | .progress {
13 | margin-bottom: 16px;
14 | height: 12px;
15 | background-image: linear-gradient(0deg, #0eb87f, #14d997);
16 | border-radius: 0.125rem;
17 | transition: width 1s ease;
18 | }
19 |
20 | .progress::after {
21 | content: '';
22 | position: absolute;
23 | top: 0;
24 | left: 0;
25 | bottom: 0;
26 | right: 0;
27 | background-image: linear-gradient(
28 | -45deg,
29 | rgba(255, 255, 255, 0.2) 25%,
30 | transparent 25%,
31 | transparent 50%,
32 | rgba(255, 255, 255, 0.2) 50%,
33 | rgba(255, 255, 255, 0.2) 75%,
34 | transparent 75%,
35 | transparent
36 | );
37 | z-index: 1;
38 | background-size: 50px 50px;
39 | animation: move 2s linear infinite;
40 | border-top-right-radius: 8px;
41 | border-bottom-right-radius: 8px;
42 | border-top-left-radius: 20px;
43 | border-bottom-left-radius: 20px;
44 | overflow: hidden;
45 | }
46 |
--------------------------------------------------------------------------------
/contentful-uploader/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/contentful-uploader/src/player.css:
--------------------------------------------------------------------------------
1 | .player {
2 | padding: 2em;
3 | }
4 |
--------------------------------------------------------------------------------
/contentful-uploader/src/preview.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Hls = require('hls.js');
3 | import './player.css';
4 |
5 | interface PlayerProps {
6 | playbackId: string;
7 | }
8 |
9 | class Player extends React.Component {
10 | playerRef: React.RefObject;
11 | hls: Hls;
12 |
13 | constructor(props: PlayerProps) {
14 | super(props);
15 |
16 | this.playerRef = React.createRef();
17 | this.hls = new Hls();
18 | }
19 |
20 | componentDidMount() {
21 | if (!this.playerRef.current) {
22 | throw Error('No reference to an existing video element found.');
23 | }
24 |
25 | if (Hls.isSupported()) {
26 | this.hls.loadSource(this.playbackUrl());
27 | this.hls.attachMedia(this.playerRef.current);
28 | } else if (
29 | this.playerRef.current.canPlayType('application/vnd.apple.mpegurl')
30 | ) {
31 | this.playerRef.current.src = this.playbackUrl();
32 | }
33 | }
34 |
35 | playbackUrl = () => `https://stream.mux.com/${this.props.playbackId}.m3u8`;
36 | posterUrl = () =>
37 | `https://image.mux.com/${this.props.playbackId}/thumbnail.jpg`;
38 |
39 | render() {
40 | return (
41 |
42 |
48 |
49 | );
50 | }
51 | }
52 |
53 | export default Player;
54 |
--------------------------------------------------------------------------------
/contentful-uploader/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "commonjs",
5 | "strict": true,
6 | "jsx": "react",
7 | "rootDir": "./src",
8 | "experimentalDecorators": true,
9 | "lib": ["dom", "es2015"],
10 | "resolveJsonModule": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/gatsby-example-graphql/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # dotenv environment variable files
55 | .env*
56 |
57 | # gatsby files
58 | .cache/
59 | public
60 |
61 | # Mac files
62 | .DS_Store
63 |
64 | # Yarn
65 | yarn-error.log
66 | .pnp/
67 | .pnp.js
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
--------------------------------------------------------------------------------
/gatsby-example-graphql/.prettierignore:
--------------------------------------------------------------------------------
1 | .cache
2 | package.json
3 | package-lock.json
4 | public
5 |
--------------------------------------------------------------------------------
/gatsby-example-graphql/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "semi": false
4 | }
5 |
--------------------------------------------------------------------------------
/gatsby-example-graphql/LICENSE:
--------------------------------------------------------------------------------
1 | The BSD Zero Clause License (0BSD)
2 |
3 | Copyright (c) 2020 Gatsby Inc.
4 |
5 | Permission to use, copy, modify, and/or distribute this software for any
6 | purpose with or without fee is hereby granted.
7 |
8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
10 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
14 | PERFORMANCE OF THIS SOFTWARE.
15 |
--------------------------------------------------------------------------------
/gatsby-example-graphql/README.md:
--------------------------------------------------------------------------------
1 | # Gatsby + Mux (GraphQL example)
2 |
3 | This is an example Gatsby site that uses [Mux](https://mux.com/) and Mux's GraphQL API powered by [OneGraph](https://www.onegraph.com/)
4 |
5 | ## Usage
6 |
7 | This project was generated with `gatsby new` based on the [hello world example](https://github.com/gatsbyjs/gatsby-starter-hello-world).
8 |
9 | To use this example you will need a free [OneGraph](https://www.onegraph.com) App and a free [Mux](https://mux.com/) account.
10 |
11 | 1. Copy `env.example` to `.env.development`. Add your Mux keys and onegraph APP ID
12 | 1. `yarn dev` will start the gatsby server
13 | 1. This app has a single index page with a list of your Mux assets
14 |
--------------------------------------------------------------------------------
/gatsby-example-graphql/env.example:
--------------------------------------------------------------------------------
1 | MUX_TOKEN_ID=xxxx
2 | MUX_TOKEN_SECRET=xxxx
3 | ONEGRAPH_APP_ID=xxxx
4 |
--------------------------------------------------------------------------------
/gatsby-example-graphql/gatsby-browser.js:
--------------------------------------------------------------------------------
1 | import "./src/styles/global.css"
2 |
3 |
--------------------------------------------------------------------------------
/gatsby-example-graphql/gatsby-config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Configure your Gatsby site with this file.
3 | *
4 | * See: https://www.gatsbyjs.org/docs/gatsby-config/
5 | */
6 |
7 | require("dotenv").config({
8 | path: `.env.${process.env.NODE_ENV}`,
9 | })
10 |
11 | module.exports = {
12 | /* Your site config here */
13 | plugins: [
14 | {
15 | resolve: "gatsby-source-graphql",
16 | options: {
17 | // Arbitrary name for the remote schema Query type
18 | typeName: "OneGraph",
19 | // Field under which the remote schema will be accessible. You'll use this in your Gatsby query
20 | fieldName: "onegraph",
21 | // Url to query from - this is the onegraph Mux API
22 | url: `https://serve.onegraph.com/dynamic?app_id=${process.env.ONEGRAPH_APP_ID}`,
23 | },
24 | },
25 | ],
26 | }
27 |
--------------------------------------------------------------------------------
/gatsby-example-graphql/gatsby-node.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Implement Gatsby's Node APIs in this file.
3 | *
4 | * See: https://www.gatsbyjs.org/docs/node-apis/
5 | */
6 |
7 | exports.createPages = async function ({ actions, graphql }) {
8 | actions.createPage({
9 | path: '/',
10 | component: require.resolve('./src/templates/index.js'),
11 | context: {
12 | tokenId: process.env.MUX_TOKEN_ID,
13 | tokenSecret: process.env.MUX_TOKEN_SECRET,
14 | }
15 | });
16 | }
17 |
--------------------------------------------------------------------------------
/gatsby-example-graphql/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gatsby-starter-hello-world",
3 | "private": true,
4 | "description": "A simplified bare-bones starter for Gatsby",
5 | "version": "0.1.0",
6 | "license": "0BSD",
7 | "scripts": {
8 | "build": "gatsby build",
9 | "develop": "gatsby develop",
10 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
11 | "start": "npm run develop",
12 | "serve": "gatsby serve",
13 | "clean": "gatsby clean",
14 | "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1",
15 | "dev": "yarn develop"
16 | },
17 | "dependencies": {
18 | "dotenv": "^8.2.0",
19 | "gatsby": "^2.24.37",
20 | "gatsby-source-graphql": "^2.7.0",
21 | "hls.js": "^0.14.8",
22 | "react": "^16.12.0",
23 | "react-dom": "^16.12.0"
24 | },
25 | "devDependencies": {
26 | "prettier": "2.0.5"
27 | },
28 | "repository": {
29 | "type": "git",
30 | "url": "https://github.com/gatsbyjs/gatsby-starter-hello-world"
31 | },
32 | "bugs": {
33 | "url": "https://github.com/gatsbyjs/gatsby/issues"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/gatsby-example-graphql/src/components/video-player.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/media-has-caption */
2 |
3 | import React, { useEffect, useRef } from 'react'
4 | import Hls from 'hls.js'
5 |
6 | export default function VideoPlayer({ src, poster }) {
7 | const videoRef = useRef(null)
8 |
9 | useEffect(() => {
10 | const video = videoRef.current
11 | if (!video) return
12 |
13 | video.controls = true
14 | let hls
15 |
16 | if (video.canPlayType('application/vnd.apple.mpegurl')) {
17 | // This will run in safari, where HLS is supported natively
18 | video.src = src
19 | } else if (Hls.isSupported()) {
20 | // This will run in all other modern browsers
21 | hls = new Hls()
22 | hls.loadSource(src)
23 | hls.attachMedia(video)
24 | } else {
25 | console.error(
26 | 'This is an old browser that does not support MSE https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API'
27 | )
28 | }
29 |
30 | return () => {
31 | if (hls) {
32 | hls.destroy()
33 | }
34 | }
35 | }, [src, videoRef])
36 |
37 | return (
38 | <>
39 |
40 | >
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/gatsby-example-graphql/src/components/video-player.module.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/gatsby-example-graphql/src/components/video-player.module.css
--------------------------------------------------------------------------------
/gatsby-example-graphql/src/styles/global.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
2 |
3 | video {
4 | width: 400px;
5 | max-width: 100%;
6 | cursor: pointer;
7 | padding-top: 40px;
8 | padding-bottom: 40px;
9 | }
10 |
11 | ul {
12 | list-style: none;
13 | }
14 |
15 | body {
16 | background: linear-gradient(90deg, rgba(251, 80, 29, 0.05) 0%, rgba(251, 36, 145, 0.05) 100%);
17 | font-family: 'Roboto', sans-serif;
18 | }
19 |
20 | .wrapper {
21 | max-width: 800px;
22 | margin: 80px auto;
23 | }
24 |
25 | h1 {
26 | text-align: center;
27 | margin-bottom: 40px;
28 | }
29 |
30 | .video-container {
31 | padding: 30px;
32 | margin: 20px 0;
33 | display: flex;
34 | align-items: center;
35 | justify-content: center;
36 | flex-direction: column;
37 | }
38 |
39 | .asset-id, .playback-id {
40 | padding: 6px 0;
41 | }
42 |
43 | .label {
44 | color: #999;
45 | }
46 |
--------------------------------------------------------------------------------
/gatsby-example-graphql/src/templates/index.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { graphql } from 'gatsby'
3 | import VideoPlayer from '../components/video-player'
4 |
5 | export default function Home({ data }) {
6 | return (
7 |
8 |
Gatsby + Mux (GraphQL example)
9 |
10 | {
11 | data.onegraph.mux.video.assets.edges.filter(({ node }) => {
12 | return node.status === 'ready' && node.playbackIds && node.playbackIds[0]
13 | }).map(({ node }) => (
14 |
15 |
16 |
17 |
18 |
19 | asset: {node.id}
20 |
21 |
22 | playback: {node.playbackIds[0].id}
23 |
24 |
25 |
26 |
27 | ))
28 | }
29 |
30 |
31 | )
32 | }
33 |
34 | export const query = graphql`
35 | query($tokenId: String!, $tokenSecret: String!) {
36 | onegraph {
37 | mux(auths: {muxAuth: {accessToken: {tokenId: $tokenId, secret: $tokenSecret}}}) {
38 | video {
39 | assets {
40 | edges {
41 | node {
42 | isTest
43 | isLive
44 | status
45 | id
46 | playbackIds {
47 | id
48 | playbackUrl
49 | thumbnail(extension:PNG time:1)
50 | }
51 | }
52 | }
53 | }
54 | }
55 | }
56 | }
57 | }
58 | `
59 |
--------------------------------------------------------------------------------
/gatsby-example-graphql/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/gatsby-example-graphql/static/favicon.ico
--------------------------------------------------------------------------------
/ios-live-streaming/.gitignore:
--------------------------------------------------------------------------------
1 | Pods/
2 | .DS_Store
3 | *~
4 | *.xcuserstate
5 | */xcuserdata/
6 |
--------------------------------------------------------------------------------
/ios-live-streaming/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Mux, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | // Copyright Mux Inc. 2020.
2 |
3 | import UIKit
4 | import AVFoundation
5 |
6 | @main
7 | class AppDelegate: UIResponder, UIApplicationDelegate {
8 |
9 | var window: UIWindow?
10 |
11 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
12 |
13 | // Initialise the Audio Session, you should do this in your AppDelegate.swift
14 | let session = AVAudioSession.sharedInstance()
15 | do {
16 | // https://stackoverflow.com/questions/51010390/avaudiosession-setcategory-swift-4-2-ios-12-play-sound-on-silent
17 | if #available(iOS 10.0, *) {
18 | try session.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth])
19 | } else {
20 | session.perform(NSSelectorFromString("setCategory:withOptions:error:"), with: AVAudioSession.Category.playAndRecord, with: [
21 | AVAudioSession.CategoryOptions.allowBluetooth,
22 | AVAudioSession.CategoryOptions.defaultToSpeaker
23 | ])
24 | try session.setMode(.default)
25 | }
26 | try session.setActive(true)
27 | } catch {
28 | print(error)
29 | }
30 |
31 | return true
32 | }
33 |
34 | // MARK: UISceneSession Lifecycle
35 |
36 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
37 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
38 | }
39 |
40 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-2.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-iTunes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/Icon-App-iTunes.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/icon-152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/icon-152.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/icon-167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/icon-167.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/icon-20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/icon-20.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/icon-29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/icon-29.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/icon-58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/icon-58.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/icon-76.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/icon-80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/AppIcon.appiconset/icon-80.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/mux-logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "mux-logo.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/mux-logo.imageset/mux-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/mux-logo.imageset/mux-logo.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/small-mux-logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "small-mux-logo.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/Assets.xcassets/small-mux-logo.imageset/small-mux-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/MuxLive/Assets.xcassets/small-mux-logo.imageset/small-mux-logo.png
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/ConnectViewController.swift:
--------------------------------------------------------------------------------
1 | // Copyright Mux Inc. 2020.
2 |
3 | import Foundation
4 | import UIKit
5 | import Loaf
6 |
7 | class ConnectViewController: UIViewController, UITextFieldDelegate {
8 |
9 | @IBOutlet weak var streamKeyTextBox: MuxTextField!
10 | @IBOutlet weak var presetSelector: UISegmentedControl!
11 | @IBOutlet weak var startCameraButton: MuxButton!
12 |
13 | // If you're testing in a tight loop, you won't want to paste a stream key each time.
14 | // Instead, set a static stream key below.
15 | let defaultStreamKey = ""
16 |
17 | var streamKey = ""
18 |
19 | // Lazily ordered in the same order that the segmented controler displays them
20 | var segmentedPresets = [BroadcastViewController.Preset.hd_1080p_30fps_5mbps,
21 | BroadcastViewController.Preset.hd_720p_30fps_3mbps,
22 | BroadcastViewController.Preset.sd_540p_30fps_2mbps,
23 | BroadcastViewController.Preset.sd_360p_30fps_1mbps]
24 |
25 | override func viewDidLoad() {
26 | super.viewDidLoad()
27 | self.streamKeyTextBox.delegate = self
28 | }
29 |
30 | // Suppress return button on the text field
31 | func textFieldShouldReturn(_ textField: UITextField) -> Bool {
32 | self.view.endEditing(true)
33 | return false
34 | }
35 |
36 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
37 | if segue.destination is BroadcastViewController
38 | {
39 | let vc = segue.destination as? BroadcastViewController
40 | vc?.streamKey = streamKey
41 | vc?.preset = segmentedPresets[presetSelector.selectedSegmentIndex]
42 | }
43 | }
44 |
45 | @IBAction func startCamera(_ sender: Any) {
46 |
47 | streamKey = streamKeyTextBox.text!
48 |
49 | // Use the hardwired default stream key if it exists, and there's nothing in the text box
50 | if streamKey == "" && defaultStreamKey != "" {
51 | streamKey = defaultStreamKey
52 | }
53 |
54 | if streamKey == "" {
55 | Loaf("Enter a Stream Key!", state: Loaf.State.warning, location: .top, sender: self).show(.short)
56 | return
57 | }
58 |
59 | performSegue(withIdentifier: "startCamera", sender: sender)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/MuxButton.swift:
--------------------------------------------------------------------------------
1 | // Copyright Mux Inc. 2020.
2 |
3 | import UIKit
4 | import Hue
5 |
6 | public class MuxButton: UIButton {
7 |
8 | override init(frame: CGRect) {
9 | super.init(frame: frame)
10 | setup()
11 | }
12 |
13 | required init?(coder: NSCoder) {
14 | super.init(coder: coder)
15 | setup()
16 | }
17 |
18 | fileprivate func setup() {
19 | self.setTitleColor(UIColor.white, for: .normal)
20 | self.layer.cornerRadius = self.frame.height * 0.25
21 | self.backgroundColor = UIColor(hex: "#fb2490")
22 | }
23 |
24 | override open var isHighlighted: Bool {
25 | didSet {
26 | backgroundColor = isHighlighted ? UIColor(hex: "#fb2490").alpha(0.7) : UIColor(hex: "#fb2490")
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/MuxTextField.swift:
--------------------------------------------------------------------------------
1 | // Copyright Mux Inc. 2020.
2 |
3 | import UIKit
4 | import Hue
5 |
6 | public class MuxTextField: UITextField, UITextFieldDelegate {
7 |
8 | override init(frame: CGRect) {
9 | super.init(frame: frame)
10 | setup()
11 | }
12 |
13 | required init?(coder: NSCoder) {
14 | super.init(coder: coder)
15 | setup()
16 | }
17 |
18 | fileprivate func setup() {
19 | self.keyboardType = .URL
20 | self.returnKeyType = .done
21 | self.autocapitalizationType = .none
22 | self.autocorrectionType = .default
23 | self.spellCheckingType = .no
24 | self.clearButtonMode = .never
25 | self.layer.cornerRadius = self.frame.height * 0.25
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/ios-live-streaming/MuxLive/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | // Copyright Mux Inc. 2020.
2 |
3 | import UIKit
4 |
5 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
6 |
7 | var window: UIWindow?
8 |
9 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
10 | guard let _ = (scene as? UIWindowScene) else { return }
11 | }
12 |
13 | func sceneDidDisconnect(_ scene: UIScene) {
14 | }
15 |
16 | func sceneDidBecomeActive(_ scene: UIScene) {
17 | }
18 |
19 | func sceneWillResignActive(_ scene: UIScene) {
20 | }
21 |
22 | func sceneWillEnterForeground(_ scene: UIScene) {
23 | }
24 |
25 | func sceneDidEnterBackground(_ scene: UIScene) {
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/ios-live-streaming/Podfile:
--------------------------------------------------------------------------------
1 | source 'https://github.com/CocoaPods/Specs.git'
2 |
3 | platform :ios, '13.0'
4 |
5 | target 'MuxLive' do
6 | use_frameworks!
7 |
8 | # Pods for MuxLive
9 | pod 'HaishinKit', '~> 1.7.1'
10 | pod 'Hue', '~> 5.0.0'
11 | pod 'Loaf'
12 |
13 | end
14 |
15 | post_install do |installer|
16 | installer.generated_projects.each do |project|
17 | project.targets.each do |target|
18 | target.build_configurations.each do |config|
19 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
20 | end
21 | end
22 | project.build_configurations.each do |config|
23 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/ios-live-streaming/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - HaishinKit (1.7.1):
3 | - Logboard (~> 2.4.1)
4 | - Hue (5.0.0)
5 | - Loaf (0.7.0)
6 | - Logboard (2.4.1)
7 |
8 | DEPENDENCIES:
9 | - HaishinKit (~> 1.7.1)
10 | - Hue (~> 5.0.0)
11 | - Loaf
12 |
13 | SPEC REPOS:
14 | https://github.com/Cocoapods/Specs.git:
15 | - HaishinKit
16 | - Hue
17 | - Loaf
18 | - Logboard
19 |
20 | SPEC CHECKSUMS:
21 | HaishinKit: a3b98b5c169b6d2dd9d363a7910828ef9b3b2022
22 | Hue: c129cb67be7d093a82bbbc30ce8a96757bf6f37a
23 | Loaf: d69937cd00649f3fa260beb8b7aef0a1feb861ef
24 | Logboard: b78984e3f9fa5075d5bfd9308d51b453992aad0e
25 |
26 | PODFILE CHECKSUM: 9b792a8c8d28ec8cf6b1afe2f4272791e3133b93
27 |
28 | COCOAPODS: 1.13.0
29 |
--------------------------------------------------------------------------------
/ios-live-streaming/screenshots/animated.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/screenshots/animated.gif
--------------------------------------------------------------------------------
/ios-live-streaming/screenshots/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/ios-live-streaming/screenshots/banner.png
--------------------------------------------------------------------------------
/mediarecorder-streaming-uploads/README.md:
--------------------------------------------------------------------------------
1 | # Mux MediaRecorder Streaming Uploads Example
2 |
3 | This project demonstrates how to record video using the MediaRecorder API, process the data into chunks, and upload the chunks to a specified server in real-time as they are recorded.
4 |
5 | ## Features
6 |
7 | - Real-time Video Recording: Records video directly from the user's webcam or mobile device.
8 | - Chunked Uploads: Processes recorded data into 8MB chunks (configurable) for efficient uploads.
9 | - Retry Logic: Retries failed uploads with exponential backoff for resilience.
10 | - Web Locks API Integration: Ensures chunks are uploaded sequentially without overlap.
11 | - Visual Feedback: Logs upload progress, buffer sizes, and errors in a scrolling log area.
12 |
13 | ## Setup
14 |
15 | ### Prerequisites
16 |
17 | - A Mux [Direct Upload URL](https://docs.mux.com/guides/upload-files-directly)
18 | - A modern browser that supports the MediaRecorder API and `navigator.mediaDevices`.
19 |
20 | ### Steps
21 |
22 | 1. Clone the repository:
23 | ```bash
24 | git clone https://github.com/muxinc/examples.git
25 | cd examples/media-recorder-streaming-uploads
26 | ```
27 |
28 | 2. Run a basic server to host the example:
29 | ```bash
30 | npx serve
31 | ```
32 |
33 | 3. Provide a valid direct upload URL in the input field.
34 |
35 | 4. Start recording by clicking the Start Recording button.
36 |
37 | 5. Stop recording by clicking the Stop Recording button.
38 |
39 | ## How it works
40 |
41 | ### 1. Start Recording
42 | - The app requests access to the user's webcam and microphone.
43 | - Initializes the MediaRecorder with the appropriate MIME type and bitrate.
44 | - Begins recording data in chunks every 500ms.
45 |
46 | ```javascript
47 | mediaRecorder.start(500); // Collect chunks every 500ms
48 | ```
49 |
50 | ### 2. Chunk Processing
51 |
52 | - Each chunk is buffered until the size exceeds CHUNK_SIZE (8MB by default).
53 | - When the buffer is full, the chunk is uploaded to the server.
54 | - If there is remaining data after a chunk upload, it stays in the buffer for the next chunk.
55 |
56 | ### 3. Uploading with Retry Logic
57 | - Chunks are uploaded using PUT requests with Content-Range headers to indicate byte ranges.
58 | - Failed uploads are retried up to 3 times with exponential backoff.
59 | - The Web Locks API is used to ensure that chunks are uploaded sequentially without overlap.
60 |
--------------------------------------------------------------------------------
/nextjs-mdx-player/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/nextjs-mdx-player/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/nextjs-mdx-player/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). It has been modified to illustrate how to embed the [Mux Player](https://www.mux.com/player) into blog posts in the [.mdx](https://mdxjs.com) file format.
2 |
3 | `utils/blogPosts.js` uses [mdx-bundler](https://github.com/kentcdodds/mdx-bundler) to process all `.mdx` files in the `blog` directory. And `pages/posts/[slug].js` renders the ` ` and imports ` ` for [Component Substitution](https://github.com/kentcdodds/mdx-bundler#component-substitution).
4 |
5 | ## Getting Started
6 |
7 | First, run the development server:
8 |
9 | ```bash
10 | npm run dev
11 | ```
12 |
13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
14 |
15 | ## Learn More
16 |
17 | To learn more about Next.js, take a look at the following resources:
18 |
19 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
20 | - [Mux Player](https://www.mux.com/player) - a video component that integrates with [Mux](https://www.mux.com).
21 | - [MDX](https://mdxjs.com) - JSX + markdown
22 |
23 | ## Deploy on Vercel
24 |
25 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
26 |
27 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
28 |
--------------------------------------------------------------------------------
/nextjs-mdx-player/blog/awesome-blog-post.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Awesome Blog Post'
3 | ---
4 |
5 | This is such an awesome blog post.
6 |
7 |
8 |
--------------------------------------------------------------------------------
/nextjs-mdx-player/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | swcMinify: true,
5 | }
6 |
7 | module.exports = nextConfig
8 |
--------------------------------------------------------------------------------
/nextjs-mdx-player/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-mdx-player-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@mux/mux-player-react": "^1.2.0",
13 | "gray-matter": "^4.0.3",
14 | "mdx-bundler": "^9.0.1",
15 | "next": "12.3.1",
16 | "react": "18.2.0",
17 | "react-dom": "18.2.0"
18 | },
19 | "devDependencies": {
20 | "eslint": "8.26.0",
21 | "eslint-config-next": "12.3.1"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/nextjs-mdx-player/pages/_app.js:
--------------------------------------------------------------------------------
1 | import "../styles/globals.css";
2 |
3 | function MyApp({ Component, pageProps }) {
4 | return ;
5 | }
6 |
7 | export default MyApp;
8 |
--------------------------------------------------------------------------------
/nextjs-mdx-player/pages/index.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import Link from "next/link";
3 | import { getPostsData } from "../utils/blogPosts";
4 |
5 | import styles from "../styles/Home.module.css";
6 |
7 | export default function Home({ allPostsData }) {
8 | return (
9 |
10 |
11 |
Create Next App
12 |
13 |
14 |
15 |
16 |
17 | Blog
18 |
19 | {allPostsData.map(({ slug, title }) => (
20 |
21 |
22 | {title}
23 |
24 |
25 | ))}
26 |
27 |
28 |
29 | );
30 | }
31 |
32 | export async function getStaticProps() {
33 | const allPostsData = getPostsData();
34 |
35 | return {
36 | props: {
37 | allPostsData,
38 | },
39 | };
40 | }
41 |
--------------------------------------------------------------------------------
/nextjs-mdx-player/pages/posts/[slug].js:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import MuxPlayer from '@mux/mux-player-react';
3 | import { getMDXComponent } from 'mdx-bundler/client';
4 | import { getAllPostSlugs, getPostData } from '../../utils/blogPosts';
5 |
6 | export const getStaticProps = async ({ params }) => {
7 | const postData = await getPostData(params.slug);
8 |
9 | return {
10 | props: {
11 | ...postData,
12 | },
13 | };
14 | };
15 |
16 | export async function getStaticPaths() {
17 | const paths = getAllPostSlugs();
18 |
19 | return {
20 | paths,
21 | fallback: false,
22 | };
23 | }
24 |
25 | export default function BlogPost({ code, frontmatter }) {
26 | const Component = useMemo(() => getMDXComponent(code), [code]);
27 |
28 | return (
29 | <>
30 | {frontmatter.title}
31 |
32 |
37 |
38 | >
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/nextjs-mdx-player/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/nextjs-mdx-player/public/favicon.ico
--------------------------------------------------------------------------------
/nextjs-mdx-player/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/nextjs-mdx-player/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 0 2rem;
3 | }
4 |
5 | .main {
6 | min-height: 100vh;
7 | padding: 4rem 0;
8 | flex: 1;
9 | display: flex;
10 | flex-direction: column;
11 | justify-content: center;
12 | align-items: center;
13 | }
14 |
15 | .heading {
16 | font-size: 1.5rem;
17 | line-height: 1.4;
18 | margin: 1rem 0;
19 | }
20 |
21 | .list {
22 | list-style: none;
23 | padding: 0;
24 | margin: 0;
25 | }
26 |
27 | .listItem {
28 | margin: 0 0 1.25rem;
29 | }
30 |
31 | .listItem a {
32 | color: blue;
33 | }
34 |
--------------------------------------------------------------------------------
/nextjs-mdx-player/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
7 | }
8 |
9 | a {
10 | color: inherit;
11 | text-decoration: none;
12 | }
13 |
14 | * {
15 | box-sizing: border-box;
16 | }
17 |
18 | @media (prefers-color-scheme: dark) {
19 | html {
20 | color-scheme: dark;
21 | }
22 | body {
23 | color: white;
24 | background: black;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/nextjs-mdx-player/utils/blogPosts.js:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import path from "path";
3 | import matter from "gray-matter";
4 | import { bundleMDX } from "mdx-bundler";
5 |
6 | const blogDirectory = path.join(process.cwd(), "blog");
7 |
8 | export function getPostsData() {
9 | const fileNames = fs.readdirSync(blogDirectory);
10 |
11 | const allPostsData = fileNames.map((fileName) => {
12 | const slug = fileName.replace(/\.mdx$/, "");
13 |
14 | const fullPath = path.join(blogDirectory, fileName);
15 | const fileContents = fs.readFileSync(fullPath, "utf8");
16 |
17 | const matterResult = matter(fileContents);
18 |
19 | return {
20 | slug,
21 | ...matterResult.data,
22 | };
23 | });
24 |
25 | return allPostsData;
26 | }
27 |
28 | export function getAllPostSlugs() {
29 | const fileNames = fs.readdirSync(blogDirectory);
30 | return fileNames.map((fileName) => {
31 | return {
32 | params: {
33 | slug: fileName.replace(/\.mdx$/, ""),
34 | },
35 | };
36 | });
37 | }
38 |
39 | export async function getPostData(slug) {
40 | const fullPath = path.join(blogDirectory, `${slug}.mdx`);
41 | const source = fs.readFileSync(fullPath, "utf8");
42 |
43 | const { code, frontmatter } = await bundleMDX({
44 | source,
45 | });
46 |
47 | return {
48 | slug,
49 | frontmatter,
50 | code,
51 | };
52 | }
53 |
--------------------------------------------------------------------------------
/nextjs-uploader-and-player/README.md:
--------------------------------------------------------------------------------
1 | Check out our full example in the official Next.js Examples repository: [with-mux-video](https://github.com/vercel/next.js/tree/canary/examples/with-mux-video)
2 |
--------------------------------------------------------------------------------
/now-airtable/.env.example:
--------------------------------------------------------------------------------
1 | AIRTABLE_API_KEY=key*********
2 | AIRTABLE_BASE=app**********
3 |
4 | MANAGEMENT_PASSWORD=your-admin-password
5 |
6 | MUX_TOKEN_ID=*******************
7 | MUX_TOKEN_SECRET=**********************************
8 |
--------------------------------------------------------------------------------
/now-airtable/README.md:
--------------------------------------------------------------------------------
1 | # Personal Video CMS
2 |
3 | Minimal personal video CMS using your own Airtable as the backend. From the user's perspective, this app allows you to:
4 |
5 | - Upload videos via a password-protected route.
6 | - Share a public list of all assets via their title and a thumbnail.
7 | - View individual videos.
8 |
9 | There are no management routes for deleting or editing assets, but you should just be going straight to your Airtable base for that!
10 |
11 | ## What you'll need to deploy this:
12 |
13 | - A copy of the [Airtable template](https://airtable.com/universe/expKKXTgmWJ76BmVL/airtable-personal-video-cms).
14 | - Your Airtable API key and base ID from above.
15 | - A Zeit account and their command line tool.
16 | - Mux Access Token.
17 |
18 | ## Deploying your own
19 |
20 | 1. Go copy the [Airtable template](https://airtable.com/universe/expKKXTgmWJ76BmVL/airtable-personal-video-cms).
21 | 2. Rename the `.env.example` `.env`
22 | ```shell
23 | $ mv .env.example .env
24 | ```
25 | 3. Update the `MANAGEMENT_PASSWORD` to be something random. You'll need to provide this password to upload new videos.
26 | 4. Get your Airtable API key and base ID. I personally like to go to the docs and get them from the example.
27 | 
28 |
29 | Update values in `.env` for `AIRTABLE_API_KEY` and `AIRTABLE_BASE` to match.
30 |
31 | 5. Go generate a new [Mux Asset Token](https://dashboard.mux.com/settings/access-tokens) and update the `MUX_TOKEN_ID` and `MUX_TOKEN_SECRET` values in `.env` to match.
32 | 6. Deploy!
33 |
34 | ```shell
35 | $ yarn deploy
36 | ```
37 |
38 | 7. **Optional**: If you want to run the UI locally, add the URL provided from the step above to your `.env` file as `BASE_URL`.
39 |
40 | ## Mux features used
41 |
42 | - [Direct Uploads](https://docs.mux.com/v1/docs/direct-upload)
43 | - [Thumbnails](https://docs.mux.com/docs/thumbnail-guide)
44 | - [Webhooks](https://docs.mux.com/docs/webhooks)
45 | - [Playback](https://docs.mux.com/docs/playback)
46 |
47 | ## Tools
48 |
49 | - [Airtable](https://airtable.com)
50 | - [Zeit Now](https://zeit.co/now)
51 | - [Next.js](https://nextjs.org)
52 |
--------------------------------------------------------------------------------
/now-airtable/api/add-video.js:
--------------------------------------------------------------------------------
1 | const { json, send } = require('micro');
2 | const { createVideo, updateVideo } = require('../utils/airtable');
3 | const decorate = require('../utils/decorate');
4 | const Mux = require('@mux/mux-node');
5 |
6 | const { Video } = new Mux();
7 |
8 | // `decorate` is a higher level function that wraps our callback to add
9 | // things like basic auth.
10 | module.exports = decorate(
11 | async (req, res) => {
12 | const params = await json(req);
13 |
14 | // Here we create our new video using our Airtable wrapper module.
15 | const videoParams = {
16 | title: params.title,
17 | description: params.description,
18 | };
19 | const video = await createVideo(videoParams);
20 |
21 | try {
22 | // Now that we have our video created internally, we can use that ID in
23 | // the `passthrough` field when we create a new asset.
24 | const upload = await Video.Uploads.create({
25 | cors_origin: req.headers.origin,
26 | new_asset_settings: {
27 | playback_policies: ['public'],
28 | passthrough: video.id, // <-- Hooray! This will come back in webhooks.
29 | },
30 | });
31 |
32 | // Now that we've successfully created our new direct upload on the Mux side
33 | // of things, let's update our internal asset to include details.
34 | const updatedVideo = await updateVideo(video.id, {
35 | status: 'waiting for upload',
36 | uploadId: upload.id,
37 | uploadUrl: upload.url,
38 | });
39 |
40 | // All that's left is to respond saying how successful we were.
41 | send(res, 201, updatedVideo);
42 | } catch (error) {
43 | console.error(error);
44 | send(res, 500, { error });
45 | }
46 | },
47 | { protected: true } // This just tells our `decorate` wrapper to use basic auth for this endpoint.
48 | );
49 |
--------------------------------------------------------------------------------
/now-airtable/api/get-video.js:
--------------------------------------------------------------------------------
1 | const { send } = require('micro');
2 | const { getVideo } = require('../utils/airtable');
3 | const decorate = require('../utils/decorate');
4 |
5 | module.exports = decorate(async (req, res) => {
6 | const id = req.query.id;
7 |
8 | try {
9 | const video = await getVideo(id);
10 |
11 | send(res, 200, video);
12 | } catch (err) {
13 | console.error(err);
14 | send(res, 500, { error: 'shrug emoji' });
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/now-airtable/api/list-videos.js:
--------------------------------------------------------------------------------
1 | const { send } = require('micro');
2 | const { listVideos } = require('../utils/airtable');
3 | const decorate = require('../utils/decorate');
4 |
5 | module.exports = decorate(async (req, res) => {
6 | try {
7 | const videos = await listVideos();
8 |
9 | send(res, 200, videos);
10 | } catch (err) {
11 | console.error(err);
12 | send(res, 500, { error: 'shrug emoji' });
13 | }
14 | });
15 |
--------------------------------------------------------------------------------
/now-airtable/api/mux-callback.js:
--------------------------------------------------------------------------------
1 | const { json, send } = require('micro');
2 | const { getVideo, updateVideo } = require('../utils/airtable');
3 |
4 | module.exports = async (req, res) => {
5 | // We'll grab the request body again, this time grabbing the event
6 | // type and event data so we can easily use it.
7 | const { type: eventType, data: eventData } = await json(req);
8 |
9 | switch (eventType) {
10 | case 'video.asset.created': {
11 | // This means an Asset was successfully created! We'll get
12 | // the existing item from the DB first, then update it with the
13 | // new Asset details
14 | const item = await getVideo(eventData.passthrough);
15 | // Just in case the events got here out of order, make sure the
16 | // asset isn't already set to ready before blindly updating it!
17 | if (item.status !== 'ready') {
18 | await updateVideo(item.id, {
19 | status: 'processing',
20 | });
21 | }
22 | break;
23 | }
24 | case 'video.asset.ready': {
25 | // This means an Asset was successfully created! This is the final
26 | // state of an Asset in this stage of its lifecycle, so we don't need
27 | // to check anything first.
28 | await updateVideo(eventData.passthrough, {
29 | status: 'ready',
30 | assetId: eventData.id,
31 | playbackId: eventData.playback_ids[0].id,
32 | });
33 | break;
34 | }
35 | case 'video.upload.cancelled': {
36 | // This fires when you decide you want to cancel an upload, so you
37 | // may want to update your internal state to reflect that it's no longer
38 | // active.
39 | await updateVideo(eventData.passthrough, { status: 'cancelled' });
40 | break;
41 | }
42 | default:
43 | // Mux sends webhooks for *lots* of things, but we'll ignore those for now
44 | console.log('some other event!', eventType, eventData);
45 | }
46 |
47 | console.log('Mux Event Handled');
48 | // Now send back that ID and the upload URL so the client can use it!
49 | send(res, 200, 'Thanks for the webhook, Mux!');
50 | };
51 |
--------------------------------------------------------------------------------
/now-airtable/components/layout.js:
--------------------------------------------------------------------------------
1 | export default ({ children }) => (
2 |
3 | {children}
4 |
5 |
12 |
13 |
19 |
20 | );
21 |
--------------------------------------------------------------------------------
/now-airtable/components/player.js:
--------------------------------------------------------------------------------
1 | import Hls from 'hls.js';
2 |
3 | class Player extends React.Component {
4 | constructor(props) {
5 | super(props);
6 |
7 | this.player = React.createRef();
8 | }
9 |
10 | playbackUrl() {
11 | return `https://stream.mux.com/${this.props.playbackId}.m3u8`;
12 | }
13 |
14 | componentDidMount() {
15 | if (Hls.isSupported()) {
16 | var hls = new Hls();
17 | hls.loadSource(this.playbackUrl());
18 | hls.attachMedia(this.player.current);
19 | } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
20 | this.player.current.src = this.playbackUrl();
21 | }
22 | }
23 |
24 | render() {
25 | return (
26 |
27 |
35 |
36 |
42 |
43 | );
44 | }
45 | }
46 |
47 | export default Player;
48 |
--------------------------------------------------------------------------------
/now-airtable/components/thumbnail.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class Thumbnail extends React.Component {
4 | onHover = () => this.setState({ hovered: true });
5 |
6 | offHover = () => this.setState({ hovered: false });
7 |
8 | imageSrc = () => {
9 | const fileAndExtension = this.state.hovered
10 | ? 'animated.gif'
11 | : 'thumbnail.png';
12 | return `https://image.mux.com/${
13 | this.props.video.playbackId
14 | }/${fileAndExtension}`;
15 | };
16 |
17 | constructor(props) {
18 | super(props);
19 |
20 | this.state = {
21 | hovered: false,
22 | };
23 | }
24 |
25 | render() {
26 | return (
27 |
28 | {this.props.video.playbackId ? (
29 |
34 | ) : (
35 |
No thumbnail for some reason
36 | )}
37 |
38 |
43 |
44 | );
45 | }
46 | }
47 |
48 | export default Thumbnail;
49 |
--------------------------------------------------------------------------------
/now-airtable/next.config.js:
--------------------------------------------------------------------------------
1 | // next.config.js
2 | module.exports = {
3 | target: 'serverless',
4 | env: {
5 | baseUrl: process.env.BASE_URL,
6 | dev: process.env.NODE_ENV !== 'production',
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/now-airtable/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "alias": ["airtable-video-cms.now.sh"],
4 | "name": "airtable-video-cms",
5 | "builds": [
6 | { "src": "api/*.js", "use": "@now/node" },
7 | { "src": "next.config.js", "use": "@now/next" }
8 | ],
9 | "routes": [
10 | {
11 | "src": "/api/videos",
12 | "dest": "/api/list-videos.js",
13 | "methods": ["GET"]
14 | },
15 | {
16 | "src": "/api/videos",
17 | "dest": "/api/add-video.js",
18 | "methods": ["POST"]
19 | },
20 | {
21 | "src": "/api/videos/?(?[^/]*)",
22 | "dest": "/api/get-video.js?id=$id",
23 | "methods": ["GET"]
24 | },
25 | {
26 | "src": "/_mux",
27 | "dest": "/api/mux-callback.js",
28 | "methods": ["POST"]
29 | }
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/now-airtable/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "api-start": "yarn micro",
4 | "api-dev": "yarn micro-dev",
5 | "ui-dev": "yarn next",
6 | "deploy:staging": "eval `cat .env` && now -e NODE_ENV=production -e AIRTABLE_API_KEY -e AIRTABLE_BASE -e MANAGEMENT_PASSWORD -e MUX_TOKEN_ID -e MUX_TOKEN_SECRET",
7 | "deploy": "yarn deploy:staging && now alias set"
8 | },
9 | "dependencies": {
10 | "@mux/mux-node": "^2.2.0",
11 | "@mux/upchunk": "^1.0.5",
12 | "airtable": "^0.7.2",
13 | "hls.js": "^0.12.3",
14 | "isomorphic-unfetch": "^3.0.0",
15 | "lodash": "^4.17.21",
16 | "micro": "^9.3.3",
17 | "micro-basic-auth": "^1.1.1",
18 | "next": "^8.0.3",
19 | "react": "^16.8.4",
20 | "react-dom": "^16.8.4"
21 | },
22 | "devDependencies": {
23 | "micro-dev": "^3.0.0",
24 | "node-env-run": "3.0.2",
25 | "now": "^14.1.1"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/now-airtable/pages/index.js:
--------------------------------------------------------------------------------
1 | import fetch from 'isomorphic-unfetch';
2 | import Link from 'next/link';
3 | import Layout from '../components/layout';
4 | import Thumbnail from '../components/thumbnail';
5 | import { colors } from '../utils/theme';
6 | import { getRequestPath } from '../utils/config';
7 |
8 | const Index = ({ videos }) => (
9 |
10 | MeTube: Now + Airtable + Mux
11 |
12 | Total Videos: {videos.length}
13 |
14 |
15 |
27 |
28 |
55 |
56 | );
57 |
58 | Index.getInitialProps = async ({ req }) => {
59 | const res = await fetch(getRequestPath(req, '/api/videos'));
60 | const allVideos = await res.json();
61 |
62 | const ready = allVideos.filter(v => v.status === 'ready');
63 | return { videos: ready };
64 | };
65 |
66 | export default Index;
67 |
--------------------------------------------------------------------------------
/now-airtable/pages/show.js:
--------------------------------------------------------------------------------
1 | import fetch from 'isomorphic-unfetch';
2 | import Link from 'next/link';
3 |
4 | import Player from '../components/player';
5 | import Layout from '../components/layout';
6 | import { colors } from '../utils/theme';
7 | import { getRequestPath } from '../utils/config';
8 |
9 | const Show = ({ video }) => (
10 |
11 |
12 |
13 | <
14 |
15 | {video.title}
16 |
17 |
18 | {video.status === 'ready' && (
19 |
22 | )}
23 |
24 | {video.description}
25 |
26 |
38 |
39 | );
40 |
41 | Show.getInitialProps = async ({ req, query }) => {
42 | const res = await fetch(getRequestPath(req, `/api/videos/${query.id}`));
43 | const video = await res.json();
44 |
45 | return { video };
46 | };
47 |
48 | export default Show;
49 |
--------------------------------------------------------------------------------
/now-airtable/utils/airtable.js:
--------------------------------------------------------------------------------
1 | const Airtable = require('airtable');
2 | const { invert } = require('lodash');
3 |
4 | Airtable.configure({ apiKey: process.env.AIRTABLE_TOKEN });
5 |
6 | const base = Airtable.base(process.env.AIRTABLE_BASE);
7 |
8 | const TABLE_NAME = 'Videos';
9 |
10 | const schemaMap = {
11 | title: 'Title',
12 | description: 'Description',
13 | uploadId: 'Upload ID',
14 | assetId: 'Asset ID',
15 | playbackId: 'Playback ID',
16 | status: 'Status',
17 | uploadUrl: 'Upload URL',
18 | };
19 |
20 | const view = item => ({
21 | id: item.getId(),
22 | ...translateSchemaFromAirtable(item.fields),
23 | });
24 |
25 | const translateSchemaToAirtable = obj =>
26 | Object.keys(obj).reduce((result, key) => {
27 | result[schemaMap[key]] = obj[key];
28 |
29 | return result;
30 | }, {});
31 |
32 | const translateSchemaFromAirtable = obj => {
33 | const invertedSchemaMap = invert(schemaMap);
34 |
35 | return Object.keys(obj).reduce((result, key) => {
36 | result[invertedSchemaMap[key]] = obj[key];
37 |
38 | return result;
39 | }, {});
40 | };
41 |
42 | const getVideo = async id => {
43 | const video = await base(TABLE_NAME).find(id);
44 |
45 | return view(video);
46 | };
47 |
48 | const createVideo = async params => {
49 | const translatedObj = translateSchemaToAirtable(params);
50 | const createdVideo = await base(TABLE_NAME).create(translatedObj);
51 |
52 | return view(createdVideo);
53 | };
54 |
55 | const updateVideo = async (id, update) => {
56 | const translatedObj = translateSchemaToAirtable(update);
57 | const updatedVideo = await base(TABLE_NAME).update(id, translatedObj);
58 |
59 | return view(updatedVideo);
60 | };
61 |
62 | const listVideos = async page => {
63 | let videos = [];
64 |
65 | await base(TABLE_NAME)
66 | .select({ sort: [{ field: 'ID', direction: 'desc' }] })
67 | .eachPage((records, fetchNextPage) => {
68 | videos = [...videos, ...records.map(view)];
69 |
70 | fetchNextPage();
71 | });
72 |
73 | return videos;
74 | };
75 |
76 | module.exports = { createVideo, getVideo, updateVideo, listVideos };
77 |
--------------------------------------------------------------------------------
/now-airtable/utils/config.js:
--------------------------------------------------------------------------------
1 | const getBaseUrl = req => {
2 | if (process.env.dev && !process.env.baseUrl) {
3 | throw new Error(
4 | 'When running the UI locally, a deployment URL is required. Did you add it to the .env file after deploying?'
5 | );
6 | }
7 |
8 | if (process.env.dev) {
9 | return process.env.baseUrl;
10 | }
11 |
12 | if (req) {
13 | return `https://${req.headers.host}`;
14 | }
15 |
16 | return '';
17 | };
18 |
19 | const getRequestPath = (req, path) => getBaseUrl(req) + path;
20 |
21 | module.exports = {
22 | getRequestPath,
23 | };
24 |
--------------------------------------------------------------------------------
/now-airtable/utils/decorate.js:
--------------------------------------------------------------------------------
1 | require('regenerator-runtime/runtime');
2 | const querystring = require('querystring');
3 | const { default: basicAuth } = require('micro-basic-auth');
4 |
5 | const authOptions = {
6 | realm: 'MeTube',
7 | validate: async (username, password, options) => {
8 | return (
9 | username === process.env.MANAGEMENT_PASSWORD ||
10 | password === process.env.MANAGEMENT_PASSWORD
11 | );
12 | },
13 | };
14 |
15 | const ALLOWED_DOMAINS = [
16 | 'http://localhost:3000',
17 | 'https://airtable-video-cms.now.sh',
18 | ];
19 |
20 | const checkOrigin = origin =>
21 | ALLOWED_DOMAINS.find(domain => domain === origin) || '';
22 |
23 | const decorate = (fn, options = {}) => (req, res) => {
24 | const [_base, query] = req.url.split('?');
25 | req.query = querystring.parse(query);
26 |
27 | res.setHeader('Access-Control-Allow-Origin', checkOrigin(req.headers.origin));
28 |
29 | if (options.protected) {
30 | return basicAuth(authOptions)(fn)(req, res);
31 | }
32 |
33 | fn(req, res);
34 | };
35 |
36 | module.exports = decorate;
37 |
--------------------------------------------------------------------------------
/now-airtable/utils/theme.js:
--------------------------------------------------------------------------------
1 | export const colors = {
2 | background: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`,
3 | primary: (opacity = 1) => `rgba(79, 124, 172, ${opacity})`,
4 | secondary: (opacity = 1) => `rgba(192, 224, 222, ${opacity})`,
5 | tertiary: (opacity = 1) => `rgba(60, 71, 75, ${opacity})`,
6 | success: (opacity = 1) => `rgba(158, 239, 229, ${opacity})`,
7 | };
8 |
--------------------------------------------------------------------------------
/remixjs-uploader-and-player/README.md:
--------------------------------------------------------------------------------
1 | Check out our full example in the official Remix.js Examples repository: [remix-run/examples/mux-video](https://github.com/remix-run/examples/tree/main/mux-video).
2 |
--------------------------------------------------------------------------------
/signed-playback-lambda/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/signed-playback-lambda/README.md:
--------------------------------------------------------------------------------
1 | # Lambda for Mux Signed Playback
2 |
3 | Streaming video from [Mux](https://mux.com/) requires a playback ID.
4 | Mux Playback IDs have two types: `public` and `signed`.
5 | `public` playback URLs can be watched anywhere, any time.
6 | `signed` playback URLs require a Mux [signing key](https://docs.mux.com/reference#url-signing-keys)
7 | which is used to generate a token via [JSON Web Tokens](https://jwt.io/).
8 |
9 | This example includes an [AWS Lambda](https://aws.amazon.com/lambda/) function that can receive a Mux [playback ID](https://docs.mux.com/reference#playback-ids)
10 | and return a signed url that's ready for use.
11 |
12 | ## In Use
13 | ```bash
14 | # Generate signed playback URL
15 | curl -X POST \
16 | -H "Content-Type: application/json" \
17 | -d '{"playbackId": "1234"}' \
18 | https://example.com/my-example-lambda
19 |
20 | # Response
21 | {
22 | "playbackId": "1234",
23 | "token": "some-token",
24 | "signedUrl": "https://stream.mux.com/1234.m3u8?token=some-token"
25 | }
26 | ```
27 |
28 | Generate a signed playback URL by making a POST request to your lambda.
29 | Pass `playbackId` and other configuration in the body of the request.
30 |
31 | ## Deployment
32 | 1. Run `yarn build` to bundle all js code and create a zip file that can be uploaded to AWS Lambda.
33 | 2. Create a new empty function in AWS Lambda. Be sure to select Node.js for the runtime option.
34 | 3. Add API Gateway as a trigger in the designer section.
35 | 4. Change the code entry type option under the Function code section to "Upload a .zip file" and upload the zip created in the dist folder.
36 | The handler reference will be `dist/lambda.handler`.
37 | 5. Generate a [Mux access token and secret](https://docs.mux.com/docs) if you have not already. Set environment variables `MUX_ACCESS_TOKEN` and `MUX_SECRET` accordingly.
38 | 6. Create a test event to confirm that your lambda works. For example:
39 | ```json
40 | {
41 | "httpMethod": "POST",
42 | "body": {
43 | "playbackId": "your-playback-id-here"
44 | }
45 | }
46 | ```
47 |
48 | ## Mux features used
49 |
50 | - [Playback](https://docs.mux.com/docs/playback)
51 | - [Signed URLs](https://docs.mux.com/docs/security-signed-urls)
52 |
53 | ## Tools
54 |
55 | - [AWS Lambda](https://aws.amazon.com/lambda/)
56 | - [@mux/mux-node](https://github.com/muxinc/mux-node-sdk)
57 |
--------------------------------------------------------------------------------
/signed-playback-lambda/lambda.js:
--------------------------------------------------------------------------------
1 | const Mux = require('@mux/mux-node');
2 | const { Video } = new Mux(process.env.MUX_ACCESS_TOKEN, process.env.MUX_SECRET);
3 | const { JWT } = Mux;
4 |
5 | exports.handler = async (event) => {
6 |
7 | // only allow POST requests
8 | if (event.httpMethod !== 'POST') {
9 | return { statusCode: 405 };
10 | }
11 |
12 | const playbackId = event.body.playbackId;
13 | const playbackType = event.body.type || 'video';
14 | // query params to be used for playback
15 | const params = event.body.params || {};
16 |
17 | // A playback ID is required to generate a signed url
18 | if (!playbackId) {
19 | return {
20 | statusCode: 400,
21 | headers: {
22 | 'Content-Type': 'application/json',
23 | },
24 | body: JSON.stringify({ message: '`playbackId` is required to generate signed url' }),
25 | };
26 | }
27 |
28 | const data = await Video.SigningKeys.create();
29 |
30 | const token = JWT.sign(playbackId, {
31 | keyId: data.id,
32 | keySecret: data.private_key,
33 | type: playbackType,
34 | // expiration expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms).
35 | expiration: '7d',
36 | params,
37 | });
38 |
39 | return {
40 | statusCode: 200,
41 | headers: {
42 | 'Content-Type': 'application/json',
43 | },
44 | body: JSON.stringify({
45 | playbackId,
46 | token,
47 | signedUrl: `https://stream.mux.com/${playbackId}.m3u8?token=${token}`,
48 | }),
49 | };
50 | };
51 |
--------------------------------------------------------------------------------
/signed-playback-lambda/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "signed-playback-lambda",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "zip": "zip -r dist/lambda.zip dist/lambda.js",
8 | "build": "webpack; yarn zip"
9 | },
10 | "dependencies": {
11 | "@mux/mux-node": "^2.2.0"
12 | },
13 | "devDependencies": {
14 | "webpack": "^4.32.0",
15 | "webpack-cli": "^3.3.2"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/signed-playback-lambda/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | target: 'node',
5 | entry: './lambda.js',
6 | output: {
7 | filename: 'lambda.js',
8 | path: path.resolve(__dirname, 'dist'),
9 | libraryTarget: 'commonjs2',
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/signed-playback-netlify/.gitignore:
--------------------------------------------------------------------------------
1 | # Local Netlify folder
2 | .netlify
3 | node_modules/
4 |
--------------------------------------------------------------------------------
/signed-playback-netlify/README.md:
--------------------------------------------------------------------------------
1 | # Netlify create mux signed urls
2 |
3 | This app uses [Netlify functions](https://www.netlify.com/docs/functions/) to create one
4 | Netlify endpoint that will 302 redirect to a signed MUX url
5 |
6 | # Usage
7 |
8 | When this is deployed you can use a netlify function as the endpoint for your player src,
9 | either in a web player or a native player.
10 |
11 | Web example
12 |
13 | ```html
14 |
15 | ```
16 |
17 | iOS AVPlayer example
18 |
19 | ```swift
20 | let url = URL(string: "https://.netlify.com/.netlify/functions/sign_playback_id?playbackId=")
21 | player = AVPlayer(url: url!)
22 | player!.play()
23 | ```
24 |
25 | The netlify endpoint will 302 redirect to the signed URL
26 |
27 | # Example (dev)
28 |
29 | 1. Run netlify dev in one terminal
30 |
31 | > netlify dev
32 |
33 | You should see a local dev server running
34 |
35 | 2. Run netlify-lambda in another terminal window
36 |
37 | > netlify-lambda server ./functions
38 |
39 | You should see that netlify-lambda watches the ./functions directory and compiles
40 | changes into your build director (.netifly/.functions)
41 |
42 | 3. Curl your local dev server
43 |
44 | ```
45 | curl -I 'http://localhost:56348/.netlify/functions/sign_playback_id?playbackId='
46 | ```
47 |
48 | You should see a 302 (redirect) with a `location` header that is the full signed mux url:
49 |
50 | `https://stream.mux.com/.m3u8?token=`
51 |
52 |
53 | # Deploy
54 |
55 | 1. Build using `netlify-lambda` and deploy with `netlify deploy`
56 |
57 | > netlify-lambda build ./functions` && netlify deploy --prod
58 |
--------------------------------------------------------------------------------
/signed-playback-netlify/functions/sign_playback_id.js:
--------------------------------------------------------------------------------
1 | const keySecret = process.env.MUX_PRIVATE_KEY
2 | import { signPlaybackId } from '../src/mux_signatures';
3 |
4 | exports.handler = async (event, context) => {
5 | try {
6 | const { queryStringParameters } = event;
7 | const { playbackId } = queryStringParameters;
8 | if (!playbackId) {
9 | return { statusCode: 400, body: JSON.stringify({errors: [{message: 'Missing playbackId in query string'}]}) };
10 | }
11 | const token = await signPlaybackId(playbackId);
12 | return {
13 | statusCode: 302,
14 | headers: {
15 | 'Access-Control-Allow-Origin': '*',
16 | location: `https://stream.mux.com/${playbackId}.m3u8?token=${token}`
17 | },
18 | body: '',
19 | }
20 | } catch (e) {
21 | console.error(e);
22 | return { statusCode: 500, body: JSON.stringify({ errors: [{message: 'Server Error'}] }) };
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/signed-playback-netlify/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | functions = "./.netlify/functions"
3 |
--------------------------------------------------------------------------------
/signed-playback-netlify/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "netlify-mux-signing",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "repository": "git@github.com:dylanjha/netlify-mux-signing.git",
6 | "author": "Dylan Jhaveri ",
7 | "license": "MIT",
8 | "private": false,
9 | "dependencies": {
10 | "@mux/mux-node": "^2.4.0"
11 | },
12 | "devDependencies": {
13 | "netlify-lambda": "^1.6.3"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/signed-playback-netlify/src/mux_signatures.js:
--------------------------------------------------------------------------------
1 | import Mux from '@mux/mux-node';
2 |
3 | export const signPlaybackId = function (playbackId) {
4 | return Mux.JWT.sign(playbackId, {
5 | keyId: process.env.MUX_SIGNING_KEY,
6 | keySecret: process.env.MUX_PRIVATE_KEY
7 | })
8 | }
9 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/.env.example:
--------------------------------------------------------------------------------
1 | MUX_TOKEN_ID=
2 | MUX_TOKEN_SECRET=
3 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/.eslintignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 |
10 | # Ignore files for PNPM, NPM and YARN
11 | pnpm-lock.yaml
12 | package-lock.json
13 | yarn.lock
14 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /** @type { import("eslint").Linter.Config } */
2 | module.exports = {
3 | root: true,
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:svelte/recommended',
8 | 'prettier'
9 | ],
10 | parser: '@typescript-eslint/parser',
11 | plugins: ['@typescript-eslint'],
12 | parserOptions: {
13 | sourceType: 'module',
14 | ecmaVersion: 2020,
15 | extraFileExtensions: ['.svelte']
16 | },
17 | env: {
18 | browser: true,
19 | es2017: true,
20 | node: true
21 | },
22 | overrides: [
23 | {
24 | files: ['*.svelte'],
25 | parser: 'svelte-eslint-parser',
26 | parserOptions: {
27 | parser: '@typescript-eslint/parser'
28 | }
29 | }
30 | ]
31 | };
32 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 | vite.config.js.timestamp-*
10 | vite.config.ts.timestamp-*
11 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore files for PNPM, NPM and YARN
2 | pnpm-lock.yaml
3 | package-lock.json
4 | yarn.lock
5 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "singleQuote": true,
4 | "trailingComma": "none",
5 | "printWidth": 100,
6 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
7 | "overrides": [
8 | {
9 | "files": "*.svelte",
10 | "options": {
11 | "parser": "svelte"
12 | }
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sveltekit-uploader-and-player",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "dev": "vite dev",
7 | "build": "vite build",
8 | "preview": "vite preview",
9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
11 | "lint": "prettier --check . && eslint .",
12 | "format": "prettier --write ."
13 | },
14 | "devDependencies": {
15 | "@sveltejs/adapter-auto": "^3.0.0",
16 | "@sveltejs/kit": "^2.0.0",
17 | "@sveltejs/vite-plugin-svelte": "^3.0.0",
18 | "@types/eslint": "^8.56.0",
19 | "@typescript-eslint/eslint-plugin": "^7.0.0",
20 | "@typescript-eslint/parser": "^7.0.0",
21 | "autoprefixer": "^10.4.16",
22 | "eslint": "^8.56.0",
23 | "eslint-config-prettier": "^9.1.0",
24 | "eslint-plugin-svelte": "^2.36.0-next.4",
25 | "postcss": "^8.4.32",
26 | "postcss-load-config": "^5.0.2",
27 | "prettier": "^3.1.1",
28 | "prettier-plugin-svelte": "^3.1.2",
29 | "prettier-plugin-tailwindcss": "^0.5.9",
30 | "svelte": "^5.0.0-next.1",
31 | "svelte-check": "^3.6.0",
32 | "tailwindcss": "^3.3.6",
33 | "tslib": "^2.4.1",
34 | "typescript": "^5.0.0",
35 | "vite": "^5.0.3"
36 | },
37 | "type": "module",
38 | "dependencies": {
39 | "@mux/mux-node": "^8.2.3",
40 | "@mux/mux-player": "^2.4.1",
41 | "@mux/mux-uploader": "^1.0.0-beta.15"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | const tailwindcss = require('tailwindcss');
2 | const autoprefixer = require('autoprefixer');
3 |
4 | const config = {
5 | plugins: [
6 | //Some plugins, like tailwindcss/nesting, need to run before Tailwind,
7 | tailwindcss(),
8 | //But others, like autoprefixer, need to run after,
9 | autoprefixer
10 | ]
11 | };
12 |
13 | module.exports = config;
14 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://kit.svelte.dev/docs/types#app
2 | // for information about these interfaces
3 | declare global {
4 | namespace App {
5 | // interface Error {}
6 | // interface Locals {}
7 | // interface PageData {}
8 | // interface PageState {}
9 | // interface Platform {}
10 | }
11 | }
12 |
13 | export {};
14 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %sveltekit.head%
8 |
9 |
10 | %sveltekit.body%
11 |
12 |
13 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/src/app.pcss:
--------------------------------------------------------------------------------
1 | /* Write your global styles here, in PostCSS syntax */
2 | @tailwind base;
3 | @tailwind components;
4 | @tailwind utilities;
5 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/src/lib/mux.ts:
--------------------------------------------------------------------------------
1 | import Mux from '@mux/mux-node';
2 | import { MUX_TOKEN_ID, MUX_TOKEN_SECRET } from '$env/static/private';
3 |
4 | const mux = new Mux({
5 | tokenId: MUX_TOKEN_ID,
6 | tokenSecret: MUX_TOKEN_SECRET
7 | });
8 |
9 | export default mux;
10 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/src/routes/+layout.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/src/routes/+page.server.ts:
--------------------------------------------------------------------------------
1 | import mux from '$lib/mux';
2 | import type { Actions, PageServerLoad } from './$types';
3 | import { redirect, fail } from '@sveltejs/kit';
4 |
5 | export const load = (async () => {
6 | // Create an endpoint for MuxUploader to upload to
7 | const upload = await mux.video.uploads.create({
8 | new_asset_settings: {
9 | playback_policy: ['public'],
10 | encoding_tier: 'baseline'
11 | },
12 | // in production, you'll want to change this origin to your-domain.com
13 | cors_origin: '*'
14 | });
15 | return { id: upload.id, url: upload.url };
16 | }) satisfies PageServerLoad;
17 |
18 | export const actions = {
19 | default: async ({ request }) => {
20 | const formData = await request.formData();
21 | const uploadId = formData.get('id');
22 | const uploadUrl = formData.get('url');
23 | if (typeof uploadId !== 'string') {
24 | return fail(400, { id: uploadId, url: uploadUrl, message: 'No uploadId found' });
25 | }
26 |
27 | // when the upload is complete,
28 | // the upload will have an assetId associated with it
29 | // we'll use that assetId to view the video status
30 | const upload = await mux.video.uploads.retrieve(uploadId);
31 | if (upload.asset_id) {
32 | redirect(303, `/status/${upload.asset_id}`);
33 | }
34 |
35 | // while onSuccess is a strong indicator that Mux has received the file
36 | // and created the asset, this isn't a guarantee.
37 | // In production, you might write an api route
38 | // to listen for the`video.upload.asset_created` webhook
39 | // https://docs.mux.com/guides/listen-for-webhooks
40 | // However, to keep things simple here,
41 | // we'll just ask the user to push the button again.
42 | // This should rarely happen.
43 | return fail(409, {
44 | id: uploadId,
45 | url: uploadUrl,
46 | message: 'Upload has no asset yet. Try again.'
47 | });
48 | }
49 | } satisfies Actions;
50 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
46 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/src/routes/api/mux/webhook/+server.ts:
--------------------------------------------------------------------------------
1 | import mux from '$lib/mux';
2 | import { json, type RequestHandler } from '@sveltejs/kit';
3 | import { MUX_WEBHOOK_SIGNING_SECRET } from '$env/static/private';
4 |
5 | // while this isn't called anywhere in this example,
6 | // It might be helpful to see what a mux webhook handler looks like.
7 |
8 | export const POST: RequestHandler = async ({ request }) => {
9 | const body = await request.text();
10 | // mux.webhooks.unwrap will validate that the given payload was sent by Mux and parse the payload.
11 | // It will also provide type-safe access to the payload.
12 | // Generate MUX_WEBHOOK_SIGNING_SECRET in the Mux dashboard
13 | // https://dashboard.mux.com/settings/webhooks
14 | const event = mux.webhooks.unwrap(body, request.headers, MUX_WEBHOOK_SIGNING_SECRET);
15 |
16 | // you can also unwrap the payload yourself:
17 | // const event = await request.json();
18 | switch (event.type) {
19 | case 'video.upload.asset_created':
20 | // we might use this to know that an upload has been completed
21 | // and we can save its assetId to our database
22 | break;
23 | case 'video.asset.ready':
24 | // we might use this to know that a video has been encoded
25 | // and we can save its playbackId to our database
26 | break;
27 | // there are many more Mux webhook events
28 | // check them out at https://docs.mux.com/webhook-reference
29 | default:
30 | break;
31 | }
32 |
33 | return json({ message: 'ok' });
34 | };
35 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/src/routes/playback/[playbackId]/+page.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 | This video is ready for playback
13 |
14 |
15 |
21 |
22 |
23 | Go
24 | back home
25 | to upload another video.
26 |
27 |
28 |
29 | {title}
30 |
31 |
32 |
33 |
34 |
38 |
39 |
40 |
41 |
42 |
43 |
47 |
48 |
49 |
54 |
55 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/src/routes/status/[assetId]/+page.server.ts:
--------------------------------------------------------------------------------
1 | import { error, redirect } from '@sveltejs/kit';
2 | import type { PageServerLoad } from './$types';
3 | import mux from '$lib/mux';
4 |
5 | export const load: PageServerLoad = (async ({ params }) => {
6 | // now that we have an assetId, we can see how the video is doing.
7 | // in production, you might have some setup where a Mux webhook
8 | // tells your server the status of your asset.
9 | // https://docs.mux.com/guides/listen-for-webhooks
10 | // for this example, however, we'll just ask the Mux API ourselves
11 | const { assetId } = params;
12 | if (typeof assetId !== 'string') {
13 | error(404, { message: 'No assetId found' });
14 | }
15 | const asset = await mux.video.assets.retrieve(assetId);
16 |
17 | // if the asset is ready and it has a public playback ID,
18 | // (which it should, considering the upload settings we used)
19 | // redirect to its playback page
20 | if (asset.status === 'ready') {
21 | const playbackIds = asset.playback_ids;
22 | if (Array.isArray(playbackIds)) {
23 | const playbackId = playbackIds.find((id) => id.policy === 'public');
24 | if (playbackId) {
25 | redirect(303, `/playback/${playbackId.id}`);
26 | }
27 | }
28 | }
29 |
30 | // if the asset is not ready, we'll keep polling
31 | return {
32 | status: asset.status,
33 | errors: asset.errors
34 | };
35 | }) satisfies PageServerLoad;
36 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/src/routes/status/[assetId]/+page.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 | {#if data.status === 'preparing'}
18 | Asset is preparing...
19 | {:else}
20 |
25 |
26 | Asset is in an unexpected state: {data.status}
.
27 |
28 | {#if Array.isArray(data.errors)}
29 |
30 | {#each data.errors as error}
31 | {JSON.stringify(error)}
32 | {/each}
33 |
34 | {/if}
35 |
36 | This is awkward. Let's
40 | refresh
41 | and try again.
42 |
43 | {/if}
44 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/sveltekit-uploader-and-player/static/favicon.png
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from '@sveltejs/adapter-auto';
2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
3 |
4 | /** @type {import('@sveltejs/kit').Config} */
5 | const config = {
6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors
7 | // for more information about preprocessors
8 | preprocess: [vitePreprocess({})],
9 |
10 | kit: {
11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
12 | // If your environment is not supported or you settled on a specific environment, switch out the adapter.
13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters.
14 | adapter: adapter()
15 | }
16 | };
17 |
18 | export default config;
19 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config}*/
2 | const config = {
3 | content: ['./src/**/*.{html,js,svelte,ts}'],
4 |
5 | theme: {
6 | extend: {}
7 | },
8 |
9 | plugins: []
10 | };
11 |
12 | module.exports = config;
13 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true,
12 | "moduleResolution": "bundler"
13 | }
14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
15 | // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
16 | //
17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
18 | // from the referenced tsconfig.json - TypeScript does not merge them in
19 | }
20 |
--------------------------------------------------------------------------------
/sveltekit-uploader-and-player/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite';
2 | import { defineConfig } from 'vite';
3 |
4 | export default defineConfig({
5 | plugins: [sveltekit()]
6 | });
7 |
--------------------------------------------------------------------------------
/swift-data-library-installation/MuxDataContainer/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/swift-data-library-installation/MuxDataContainer/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/swift-data-library-installation/MuxDataContainer/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.8
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "MuxDataContainer",
8 | platforms: [
9 | .iOS(.v15),
10 | .macCatalyst(.v15)
11 | ],
12 | products: [
13 | // Products define the executables and libraries a package produces, making them visible to other packages.
14 | .library(
15 | name: "MuxDataContainer",
16 | targets: [
17 | "MuxDataContainer"
18 | ]
19 | ),
20 | ],
21 | dependencies: [
22 | .package(
23 | url: "https://github.com/muxinc/mux-stats-sdk-avplayer.git",
24 | from: "3.3.2"
25 | )
26 | ],
27 | targets: [
28 | // Targets are the basic building blocks of a package, defining a module or a test suite.
29 | // Targets can depend on other targets in this package and products from dependencies.
30 | .target(
31 | name: "MuxDataContainer",
32 | dependencies: [
33 | .product(
34 | name: "MUXSDKStats",
35 | package: "mux-stats-sdk-avplayer"
36 | )
37 | ]
38 | ),
39 | .testTarget(
40 | name: "MuxDataContainerTests",
41 | dependencies: [
42 | "MuxDataContainer"
43 | ]
44 | ),
45 | ]
46 | )
47 |
--------------------------------------------------------------------------------
/swift-data-library-installation/MuxDataContainer/Sources/MuxDataContainer/MonitoringContainer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MonitoringContainer.swift
3 | //
4 | //
5 |
6 | import AVFoundation
7 | import AVKit
8 | import Foundation
9 |
10 | import MuxCore
11 | import MUXSDKStats
12 |
13 | public class MonitoringContainer {
14 |
15 | public static let shared = MonitoringContainer()
16 |
17 | public func monitor(
18 | playerViewController: AVPlayerViewController,
19 | playerName: String,
20 | videoID: String,
21 | videoTitle: String,
22 | environmentKey: String?
23 | ) {
24 |
25 | let playerData = MUXSDKCustomerPlayerData()
26 | playerData.environmentKey = environmentKey
27 |
28 | let videoData = MUXSDKCustomerVideoData()
29 | videoData.videoId = videoID
30 | videoData.videoTitle = videoTitle
31 |
32 | let customerData = MUXSDKCustomerData(
33 | customerPlayerData: playerData,
34 | videoData: videoData,
35 | viewData: nil
36 | )!
37 |
38 | MUXSDKStats.monitorAVPlayerViewController(
39 | playerViewController,
40 | withPlayerName: playerName,
41 | customerData: customerData
42 | )
43 | }
44 |
45 | public func monitor(
46 | playerLayer: AVPlayerLayer,
47 | playerName: String,
48 | videoID: String,
49 | videoTitle: String,
50 | environmentKey: String?
51 | ) {
52 |
53 | let playerData = MUXSDKCustomerPlayerData()
54 | playerData.environmentKey = environmentKey
55 |
56 | let videoData = MUXSDKCustomerVideoData()
57 | videoData.videoId = videoID
58 | videoData.videoTitle = videoTitle
59 |
60 | let customerData = MUXSDKCustomerData(
61 | customerPlayerData: playerData,
62 | videoData: videoData,
63 | viewData: nil
64 | )!
65 |
66 | MUXSDKStats.monitorAVPlayerLayer(
67 | playerLayer,
68 | withPlayerName: playerName,
69 | customerData: customerData
70 | )
71 | }
72 |
73 | public func destroyPlayer(
74 | _ playerName: String
75 | ) {
76 | MUXSDKStats.destroyPlayer(playerName)
77 | }
78 |
79 | }
80 |
--------------------------------------------------------------------------------
/swift-data-library-installation/MuxDataContainer/Sources/MuxDataContainer/ProcessInfo+Mux.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProcessInfo+Mux.swift
3 | // SwiftDataLibraryInstallation
4 | //
5 |
6 | import Foundation
7 |
8 | public extension ProcessInfo {
9 | var environmentKey: String? {
10 | guard let value = environment["ENV_KEY"],
11 | !value.isEmpty else {
12 | return nil
13 | }
14 |
15 | return value
16 | }
17 |
18 | var playbackID: String? {
19 | guard let value = environment["PLAYBACK_ID"],
20 | !value.isEmpty else {
21 | return nil
22 | }
23 |
24 | return value
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/swift-data-library-installation/MuxDataContainer/Tests/MuxDataContainerTests/MuxDataContainerTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import MuxDataContainer
3 |
4 | final class MuxDataContainerTests: XCTestCase {
5 | func testExample() throws {
6 | // XCTest Documentation
7 | // https://developer.apple.com/documentation/xctest
8 |
9 | // Defining Test Cases and Test Methods
10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/swift-data-library-installation/README.md:
--------------------------------------------------------------------------------
1 | # Integrating Mux Data in multiple applications
2 |
3 | If you intend to use Mux Data across multiple iOS, iPadOS, and Mac Catalyst applications, code specific to Mux Data can be shared using [Swift Package Manager](https://www.swift.org/package-manager/).
4 |
5 | This example application installs the Mux Data SDK for AVPlayer as a dependency to a Swift package library target: `MuxDataContainer`.
6 |
7 | ## Using Swift Package Manager with Xcode
8 |
9 | See the Xcode guide for [adding package dependencies to your app](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app) on how to install Swift Package Manager dependencies for your application using Xcode.
10 |
11 | ## Best practices when adapting this example
12 |
13 | `MuxDataContainer` in this example is only a passthrough wrapper that wraps `MUXSDKStats` APIs for starting and stopping player monitoring. When using this pattern for your own applications, `MuxDataContainer` is where you'd place code intended to be used alongside Mux Data that is specific to your use case and shared across multiple applications.
14 |
15 | For simplicity, this example confgures `MuxDataContainer` as a local package as described in [organizing your code with local packages](https://developer.apple.com/documentation/xcode/organizing-your-code-with-local-packages). A local package is suitable for an initial test and does not require a separate git repository for distribution.
16 |
17 | ### Not using Swift?
18 |
19 | Note: *Although this example is in Swift, Swift Package Manager supports reusing Swift, Objective-C, Objective-C++, C, or C++ components. Generally, as of the time of writing, SPM does not support mixing multiple languages in the same SPM target, consult Apple and SPM documentation linked throughout for more details.*
20 |
--------------------------------------------------------------------------------
/swift-data-library-installation/SwiftDataLibraryInstallation/SwiftDataLibraryInstallation.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/swift-data-library-installation/SwiftDataLibraryInstallation/SwiftDataLibraryInstallation.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/swift-data-library-installation/SwiftDataLibraryInstallation/SwiftDataLibraryInstallation/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // SwiftDataLibraryInstallation
4 | //
5 |
6 | import UIKit
7 |
8 | @main
9 | class AppDelegate: UIResponder, UIApplicationDelegate {
10 |
11 |
12 |
13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
14 | // Override point for customization after application launch.
15 | return true
16 | }
17 |
18 | // MARK: UISceneSession Lifecycle
19 |
20 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
21 | // Called when a new scene session is being created.
22 | // Use this method to select a configuration to create the new scene with.
23 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
24 | }
25 |
26 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
27 | // Called when the user discards a scene session.
28 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
29 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
30 | }
31 |
32 |
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/swift-data-library-installation/SwiftDataLibraryInstallation/SwiftDataLibraryInstallation/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/swift-data-library-installation/SwiftDataLibraryInstallation/SwiftDataLibraryInstallation/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/swift-data-library-installation/SwiftDataLibraryInstallation/SwiftDataLibraryInstallation/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/swift-data-library-installation/SwiftDataLibraryInstallation/SwiftDataLibraryInstallation/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/swift-data-library-installation/SwiftDataLibraryInstallation/SwiftDataLibraryInstallation/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIApplicationSceneManifest
6 |
7 | UIApplicationSupportsMultipleScenes
8 |
9 | UISceneConfigurations
10 |
11 | UIWindowSceneSessionRoleApplication
12 |
13 |
14 | UISceneConfigurationName
15 | Default Configuration
16 | UISceneDelegateClassName
17 | $(PRODUCT_MODULE_NAME).SceneDelegate
18 | UISceneStoryboardFile
19 | Main
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/swift-data-library-installation/SwiftDataLibraryInstallation/SwiftDataLibraryInstallationTests/SwiftDataLibraryInstallationTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftDataLibraryInstallationTests.swift
3 | // SwiftDataLibraryInstallationTests
4 | //
5 |
6 | import XCTest
7 | @testable import SwiftDataLibraryInstallation
8 |
9 | final class SwiftDataLibraryInstallationTests: XCTestCase {
10 |
11 | override func setUpWithError() throws {
12 | // Put setup code here. This method is called before the invocation of each test method in the class.
13 | }
14 |
15 | override func tearDownWithError() throws {
16 | // Put teardown code here. This method is called after the invocation of each test method in the class.
17 | }
18 |
19 | func testExample() throws {
20 | // This is an example of a functional test case.
21 | // Use XCTAssert and related functions to verify your tests produce the correct results.
22 | // Any test you write for XCTest can be annotated as throws and async.
23 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
24 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
25 | }
26 |
27 | func testPerformanceExample() throws {
28 | // This is an example of a performance test case.
29 | self.measure {
30 | // Put the code you want to measure the time of here.
31 | }
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/swift-data-library-installation/SwiftDataLibraryInstallation/SwiftDataLibraryInstallationUITests/SwiftDataLibraryInstallationUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftDataLibraryInstallationUITests.swift
3 | // SwiftDataLibraryInstallationUITests
4 | //
5 |
6 | import XCTest
7 |
8 | final class SwiftDataLibraryInstallationUITests: XCTestCase {
9 |
10 | override func setUpWithError() throws {
11 | // Put setup code here. This method is called before the invocation of each test method in the class.
12 |
13 | // In UI tests it is usually best to stop immediately when a failure occurs.
14 | continueAfterFailure = false
15 |
16 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
17 | }
18 |
19 | override func tearDownWithError() throws {
20 | // Put teardown code here. This method is called after the invocation of each test method in the class.
21 | }
22 |
23 | func testExample() throws {
24 | // UI tests must launch the application that they test.
25 | let app = XCUIApplication()
26 | app.launch()
27 |
28 | // Use XCTAssert and related functions to verify your tests produce the correct results.
29 | }
30 |
31 | func testLaunchPerformance() throws {
32 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
33 | // This measures how long it takes to launch your application.
34 | measure(metrics: [XCTApplicationLaunchMetric()]) {
35 | XCUIApplication().launch()
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/swift-data-library-installation/SwiftDataLibraryInstallation/SwiftDataLibraryInstallationUITests/SwiftDataLibraryInstallationUITestsLaunchTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftDataLibraryInstallationUITestsLaunchTests.swift
3 | // SwiftDataLibraryInstallationUITests
4 | //
5 |
6 | import XCTest
7 |
8 | final class SwiftDataLibraryInstallationUITestsLaunchTests: XCTestCase {
9 |
10 | override class var runsForEachTargetApplicationUIConfiguration: Bool {
11 | true
12 | }
13 |
14 | override func setUpWithError() throws {
15 | continueAfterFailure = false
16 | }
17 |
18 | func testLaunch() throws {
19 | let app = XCUIApplication()
20 | app.launch()
21 |
22 | // Insert steps here to perform after app launch but before taking a screenshot,
23 | // such as logging into a test account or navigating somewhere in the app
24 |
25 | let attachment = XCTAttachment(screenshot: app.screenshot())
26 | attachment.name = "Launch Screen"
27 | attachment.lifetime = .keepAlways
28 | add(attachment)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/swift-video-app/MuxExampleVideoApp/MuxExampleVideoApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/swift-video-app/MuxExampleVideoApp/MuxExampleVideoApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/swift-video-app/MuxExampleVideoApp/MuxExampleVideoApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "mux-stats-sdk-avplayer",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/muxinc/mux-stats-sdk-avplayer.git",
7 | "state" : {
8 | "revision" : "5a310713d8330cb2c8e4755d9a80088697536d65",
9 | "version" : "3.3.2"
10 | }
11 | },
12 | {
13 | "identity" : "stats-sdk-objc",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/muxinc/stats-sdk-objc.git",
16 | "state" : {
17 | "revision" : "a1f6cf4e20e71257c8657bbbff1ac9771bb39a52",
18 | "version" : "4.5.2"
19 | }
20 | }
21 | ],
22 | "version" : 2
23 | }
24 |
--------------------------------------------------------------------------------
/swift-video-app/MuxExampleVideoApp/MuxExampleVideoApp.xcodeproj/project.xcworkspace/xcuserdata/dylanjhaveri.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/swift-video-app/MuxExampleVideoApp/MuxExampleVideoApp.xcodeproj/project.xcworkspace/xcuserdata/dylanjhaveri.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/swift-video-app/MuxExampleVideoApp/MuxExampleVideoApp.xcodeproj/xcuserdata/dylanjhaveri.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | MuxExampleVideoApp.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/swift-video-app/MuxExampleVideoApp/MuxExampleVideoApp/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
--------------------------------------------------------------------------------
/swift-video-app/MuxExampleVideoApp/MuxExampleVideoApp/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // MuxExampleVideoApp
4 | //
5 | // Created by Dylan Jhaveri on 4/7/21.
6 | //
7 |
8 | import UIKit
9 | import AVFoundation
10 |
11 | @main
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
15 | let audioSession = AVAudioSession.sharedInstance()
16 | do {
17 | try audioSession.setCategory(AVAudioSession.Category.playback)
18 | } catch {
19 | print("Setting category to AVAudioSessionCategoryPlayback failed.")
20 | }
21 | return true
22 | }
23 |
24 | // MARK: UISceneSession Lifecycle
25 |
26 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
27 | // Called when a new scene session is being created.
28 | // Use this method to select a configuration to create the new scene with.
29 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
30 | }
31 |
32 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
33 | // Called when the user discards a scene session.
34 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
35 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
36 | }
37 |
38 |
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/swift-video-app/MuxExampleVideoApp/MuxExampleVideoApp/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/swift-video-app/MuxExampleVideoApp/MuxExampleVideoApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/swift-video-app/MuxExampleVideoApp/MuxExampleVideoApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/swift-video-app/MuxExampleVideoApp/MuxExampleVideoApp/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/swift-video-app/MuxExampleVideoApp/MuxExampleVideoApp/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UISceneConfigurationName
33 | Default Configuration
34 | UISceneDelegateClassName
35 | $(PRODUCT_MODULE_NAME).SceneDelegate
36 | UISceneStoryboardFile
37 | Main
38 |
39 |
40 |
41 |
42 | UIApplicationSupportsIndirectInputEvents
43 |
44 | UIBackgroundModes
45 |
46 | audio
47 |
48 | UILaunchStoryboardName
49 | LaunchScreen
50 | UIMainStoryboardFile
51 | Main
52 | UIRequiredDeviceCapabilities
53 |
54 | armv7
55 |
56 | UISupportedInterfaceOrientations
57 |
58 | UIInterfaceOrientationPortrait
59 | UIInterfaceOrientationLandscapeLeft
60 | UIInterfaceOrientationLandscapeRight
61 |
62 | UISupportedInterfaceOrientations~ipad
63 |
64 | UIInterfaceOrientationPortrait
65 | UIInterfaceOrientationPortraitUpsideDown
66 | UIInterfaceOrientationLandscapeLeft
67 | UIInterfaceOrientationLandscapeRight
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/swift-video-app/MuxExampleVideoApp/MuxExampleVideoAppTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/swift-video-app/MuxExampleVideoApp/MuxExampleVideoAppTests/MuxExampleVideoAppTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MuxExampleVideoAppTests.swift
3 | // MuxExampleVideoAppTests
4 | //
5 | // Created by Dylan Jhaveri on 4/7/21.
6 | //
7 |
8 | import XCTest
9 | @testable import MuxExampleVideoApp
10 |
11 | class MuxExampleVideoAppTests: XCTestCase {
12 |
13 | override func setUpWithError() throws {
14 | // Put setup code here. This method is called before the invocation of each test method in the class.
15 | }
16 |
17 | override func tearDownWithError() throws {
18 | // Put teardown code here. This method is called after the invocation of each test method in the class.
19 | }
20 |
21 | func testExample() throws {
22 | // This is an example of a functional test case.
23 | // Use XCTAssert and related functions to verify your tests produce the correct results.
24 | }
25 |
26 | func testPerformanceExample() throws {
27 | // This is an example of a performance test case.
28 | self.measure {
29 | // Put the code you want to measure the time of here.
30 | }
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/swift-video-app/MuxExampleVideoApp/MuxExampleVideoAppUITests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/swift-video-app/MuxExampleVideoApp/MuxExampleVideoAppUITests/MuxExampleVideoAppUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MuxExampleVideoAppUITests.swift
3 | // MuxExampleVideoAppUITests
4 | //
5 | // Created by Dylan Jhaveri on 4/7/21.
6 | //
7 |
8 | import XCTest
9 |
10 | class MuxExampleVideoAppUITests: XCTestCase {
11 |
12 | override func setUpWithError() throws {
13 | // Put setup code here. This method is called before the invocation of each test method in the class.
14 |
15 | // In UI tests it is usually best to stop immediately when a failure occurs.
16 | continueAfterFailure = false
17 |
18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
19 | }
20 |
21 | override func tearDownWithError() throws {
22 | // Put teardown code here. This method is called after the invocation of each test method in the class.
23 | }
24 |
25 | func testExample() throws {
26 | // UI tests must launch the application that they test.
27 | let app = XCUIApplication()
28 | app.launch()
29 |
30 | // Use recording to get started writing UI tests.
31 | // Use XCTAssert and related functions to verify your tests produce the correct results.
32 | }
33 |
34 | func testLaunchPerformance() throws {
35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) {
36 | // This measures how long it takes to launch your application.
37 | measure(metrics: [XCTApplicationLaunchMetric()]) {
38 | XCUIApplication().launch()
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/swift-video-app/README.md:
--------------------------------------------------------------------------------
1 | # swift-video-app
2 |
3 | This is a swift app that uses AVPlayer, AVPlayerViewController and MuxData. The video source is a Mux Video HLS manifest, but that can be swapped out for any AVPlayer compatible source.
4 |
5 | The main thing to test here was to get the audio to continue playing when the application enters the background. We wrote about it on [the Mux blog](mux.com/blog/background-audio-handling-with-ios-avplayer).
6 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/.env.example:
--------------------------------------------------------------------------------
1 | MUX_WEBHOOK_SECRET=
2 | KNOCK_SECRET_KEY=
3 | NEXT_PUBLIC_KNOCK_API_KEY="sk_12345"
4 | NEXT_PUBLIC_KNOCK_USER_ID="adfasf"
5 | NEXT_PUBLIC_KNOCK_FEED_CHANNEL_ID="asdfsdfa"
6 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env.local
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/README.md:
--------------------------------------------------------------------------------
1 | ## References
2 |
3 | ### SDKs
4 | https://github.com/knocklabs/knock-node
5 | https://github.com/muxinc/mux-node-sdk
6 |
7 | ### Docs
8 | https://docs.knock.app/concepts/objects
9 | https://docs.mux.com/
--------------------------------------------------------------------------------
/webhook-notifications-knock/app/api/knock/channels/route.ts:
--------------------------------------------------------------------------------
1 | import { revalidatePath } from 'next/cache';
2 | import { headers } from 'next/headers';
3 |
4 | import { Knock } from '@knocklabs/node';
5 | const knockClient = new Knock(process.env.KNOCK_SECRET_KEY);
6 |
7 | const channels = [
8 | {
9 | id: 'knock',
10 | properties: {
11 | name: 'Knock',
12 | icon: '🔔',
13 | YouTube: 'https://www.youtube.com/channel/UCDScOjujAI3-_9iJ0Me0Aug',
14 | Twitter: 'https://twitter.com/knocklabs',
15 | },
16 | },
17 | {
18 | id: 'mux',
19 | properties: {
20 | name: 'Mux',
21 | icon: '📹',
22 | YouTube: 'https://www.youtube.com/@MuxHQ',
23 | Twitter: 'https://twitter.com/MuxHQ',
24 | },
25 | },
26 | ];
27 |
28 | export async function POST(request: Request) {
29 | const body = await request.text();
30 | const payload = JSON.parse(body);
31 | if (payload.action === 'create') {
32 | channels.forEach(async (channel) => {
33 | await knockClient.objects.set('channels', channel.id, channel.properties);
34 | });
35 | }
36 |
37 | if (payload.action === 'delete') {
38 | channels.forEach(async (channel) => {
39 | await knockClient.objects.delete('channels', channel.id);
40 | });
41 | }
42 | return Response.json({ message: 'ok' });
43 | }
44 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/app/api/knock/on-play/route.ts:
--------------------------------------------------------------------------------
1 | import { revalidatePath } from 'next/cache';
2 | import { headers } from 'next/headers';
3 |
4 | import { Knock } from '@knocklabs/node';
5 | const knockClient = new Knock(process.env.KNOCK_SECRET_KEY);
6 |
7 | export async function POST(request: Request) {
8 | const body = await request.text();
9 | const inlineUser = {
10 | id: 'fde69556-a3a5-4812-a3e9-c9ded0cbc950',
11 | name: 'John Hammond',
12 | email: 'hammond@ingen.net',
13 | };
14 | knockClient.workflows.trigger('new-view', {
15 | recipients: [process.env.NEXT_PUBLIC_KNOCK_USER_ID as string],
16 | data: JSON.parse(body),
17 | });
18 |
19 | return Response.json({ message: 'ok' });
20 | }
21 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/app/api/knock/subscriptions/route.ts:
--------------------------------------------------------------------------------
1 | import { revalidatePath } from 'next/cache';
2 | import { headers } from 'next/headers';
3 |
4 | import { Knock } from '@knocklabs/node';
5 | const knockClient = new Knock(process.env.KNOCK_SECRET_KEY);
6 |
7 | const subscriptions = [
8 | {
9 | id: '9d17eb2d-1980-4614-bcfe-5696ca9be631',
10 | properties: {
11 | role: 'regular',
12 | },
13 | },
14 | {
15 | id: 'a12896b9-658b-43ff-8d5b-79c34215778a',
16 | properties: {
17 | role: 'allAccess',
18 | },
19 | },
20 | ];
21 |
22 | export async function POST(request: Request) {
23 | const body = await request.text();
24 | const payload = JSON.parse(body);
25 | if (payload.action === 'create') {
26 | channels.forEach(async (channel) => {
27 | subscriptions.forEach(async (subscription) => {
28 | await knockClient.objects.addSubscriptions('channels', channel.id, {
29 | recipients: [subscription.id],
30 | properties: subscription.properties,
31 | });
32 | });
33 | });
34 | }
35 |
36 | if (payload.action === 'delete') {
37 | channels.forEach(async (channel) => {
38 | subscriptions.forEach(async (subscription) => {
39 | await knockClient.objects.deleteSubscriptions('channels', channel.id, {
40 | recipients: [subscription.id],
41 | });
42 | });
43 | });
44 | }
45 | return Response.json({ message: 'ok' });
46 | }
47 | const channels = [
48 | {
49 | id: 'knock',
50 | properties: {
51 | name: 'Knock',
52 | icon: '🔔',
53 | YouTube: 'https://www.youtube.com/channel/UCDScOjujAI3-_9iJ0Me0Aug',
54 | Twitter: 'https://twitter.com/knocklabs',
55 | },
56 | },
57 | {
58 | id: 'mux',
59 | properties: {
60 | name: 'Mux',
61 | icon: '📹',
62 | YouTube: 'https://www.youtube.com/@MuxHQ',
63 | Twitter: 'https://twitter.com/MuxHQ',
64 | },
65 | },
66 | ];
67 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/app/api/webhooks/mux/route.ts:
--------------------------------------------------------------------------------
1 | import { revalidatePath } from 'next/cache';
2 | import { headers } from 'next/headers';
3 |
4 | import Mux from '@mux/mux-node';
5 |
6 | import { Knock } from '@knocklabs/node';
7 | const knockClient = new Knock(process.env.KNOCK_SECRET_KEY);
8 |
9 | // const mux = new Mux({
10 | // webhookSecret: process.env.MUX_WEBHOOK_SECRET,
11 | // });
12 |
13 | export async function POST(request: Request) {
14 | const headersList = headers();
15 | const body = await request.text();
16 | const event = JSON.parse(body);
17 | console.log(event);
18 | // const event = mux.webhooks.unwrap(body, headersList);
19 |
20 | switch (event.type) {
21 | case 'video.live_stream.active':
22 | console.log('📺 live stream started.....');
23 | const channel = await knockClient.objects.get('channels', 'knock');
24 | const { workflow_run_id } = await knockClient.workflows.trigger(
25 | 'new-stream',
26 | {
27 | recipients: [{ collection: 'channels', id: 'knock' }],
28 | data: {
29 | channel,
30 | },
31 | }
32 | );
33 | break;
34 | case 'video.asset.ready':
35 | console.log('🎞️ new asset ready.....');
36 |
37 | console.log(`📨 new workflow run started: ${workflow_run_id}`);
38 | break;
39 | default:
40 | break;
41 | }
42 |
43 | return Response.json({ message: 'ok' });
44 | }
45 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/app/api/webhooks/mux/video.asset.ready.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "video.asset.ready",
3 | "request_id": null,
4 | "object": {
5 | "type": "asset",
6 | "id": "IlEvde8ZuiOOjOZP9RjDC5k4kv00WZT3B"
7 | },
8 | "id": "072d5897-baeb-4b5b-b10e-72399463ed35",
9 | "environment": {
10 | "name": "Testing",
11 | "id": "j29sb4"
12 | },
13 | "data": {
14 | "upload_id": "fib5TohlksKstELSGJHiGWTLkzTX72f00",
15 | "tracks": [
16 | {
17 | "type": "video",
18 | "max_width": 1920,
19 | "max_height": 1080,
20 | "max_frame_rate": 29.926,
21 | "id": "Xc2dXkAjz01sJNuExusNdT6jPycir48Na",
22 | "duration": 10.024767
23 | },
24 | {
25 | "type": "audio",
26 | "status": "ready",
27 | "primary": true,
28 | "name": "Default",
29 | "max_channels": 2,
30 | "language_code": "und",
31 | "id": "n4wBZBHPdmdCHT1RbsTgxBS6Ig3U40202Z5Tyh2Cze2HNTZNqR3ST1gA"
32 | }
33 | ],
34 | "status": "ready",
35 | "resolution_tier": "1080p",
36 | "playback_ids": [
37 | {
38 | "policy": "public",
39 | "id": "SJOkDeyn6asBEJ735VfQ7tQ6qotMhmkY"
40 | }
41 | ],
42 | "mp4_support": "none",
43 | "max_stored_resolution": "HD",
44 | "max_stored_frame_rate": 29.926,
45 | "max_resolution_tier": "1080p",
46 | "master_access": "none",
47 | "ingest_type": "on_demand_direct_upload",
48 | "id": "IlEvde8ZuiOOjOZP9RjDC5k4kv00WZT3B",
49 | "encoding_tier": "baseline",
50 | "duration": 10.066667,
51 | "created_at": 1712262539,
52 | "aspect_ratio": "16:9"
53 | },
54 | "created_at": "2024-04-04T20:29:02.294000Z",
55 | "attempts": [],
56 | "accessor_source": null,
57 | "accessor": null
58 | }
--------------------------------------------------------------------------------
/webhook-notifications-knock/app/api/webhooks/mux/video.live_stream.active.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "video.live_stream.active",
3 | "request_id": null,
4 | "object": {
5 | "type": "live",
6 | "id": "1TaauZVrUfXwkpUxKVWQiSOD02YWhTpan"
7 | },
8 | "id": "072d5897-baeb-4b5b-b10e-72399463ed35",
9 | "environment": {
10 | "name": "Testing",
11 | "id": "j29sb4"
12 | },
13 | "data": {
14 | "stream_key": "******",
15 | "status": "active",
16 | "srt_passphrase": "******",
17 | "simulcast_targets": [
18 | {
19 | "url": "rtmps://va.pscp.tv:443/x",
20 | "stream_key": "******",
21 | "status": "broadcasting",
22 | "passthrough": "Twitter",
23 | "id": "PYbkjeAddS5JPo1kLRdmjLP5Zu6bb9yMQ8VG00ekC7mCrn6HtZFtRWw"
24 | },
25 | {
26 | "url": "rtmp://a.rtmp.youtube.com/live2",
27 | "stream_key": "******",
28 | "status": "broadcasting",
29 | "passthrough": "YouTube",
30 | "id": "xl9IyCxhrfXKkTbmm5Pk6wez7R48ClywWLt1B1EgMyM39Duevx4Deg"
31 | }
32 | ],
33 | "recording": true,
34 | "recent_asset_ids": [],
35 | "playback_ids": [
36 | {
37 | "policy": "public",
38 | "id": "qzrAqvB41wlq5d2hYJ6RT6Y00KRl2Gf2k"
39 | }
40 | ],
41 | "new_asset_settings": {
42 | "playback_policies": [
43 | "public"
44 | ]
45 | },
46 | "max_continuous_duration": 43200,
47 | "low_latency": true,
48 | "latency_mode": "low",
49 | "id": "1TaauZVrUfXwkpUxKVWQiSOD02YWhTpan",
50 | "created_at": 1706884405,
51 | "connected": true,
52 | "active_ingest_protocol": "rtmp",
53 | "active_asset_id": "uGTBjfqFSPO4kgAj6Sll01LITWMLf500Gl"
54 | },
55 | "created_at": "2024-04-04T19:00:37.613000Z",
56 | "attempts": [],
57 | "accessor_source": null,
58 | "accessor": null
59 | }
--------------------------------------------------------------------------------
/webhook-notifications-knock/app/components/FeedContainer.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useState, useRef, useEffect } from 'react';
3 | import {
4 | KnockProvider,
5 | KnockFeedProvider,
6 | NotificationIconButton,
7 | NotificationFeedPopover,
8 | BellIcon,
9 | } from '@knocklabs/react';
10 |
11 | // Required CSS import, unless you're overriding the styling
12 | import '@knocklabs/react/dist/index.css';
13 | import FeedToasts from './FeedToasts';
14 |
15 | const FeedContainer = () => {
16 | const [isVisible, setIsVisible] = useState(false);
17 | const [isClient, setIsClient] = useState(false);
18 | const notifButtonRef = useRef(null);
19 | useEffect(() => {
20 | setIsClient(true);
21 | }, []);
22 | return isClient ? (
23 |
27 |
31 | <>
32 | setIsVisible(!isVisible)}
35 | />
36 | setIsVisible(false)}
40 | />
41 | {/* */}
42 | >
43 |
44 |
45 | ) : (
46 |
47 | );
48 | };
49 |
50 | export default FeedContainer;
51 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/app/components/FeedToasts.tsx:
--------------------------------------------------------------------------------
1 | import toast, { Toaster } from 'react-hot-toast';
2 | import { useEffect } from 'react';
3 | import { useKnockFeed } from '@knocklabs/react';
4 |
5 | const FeedToasts = () => {
6 | const { feedClient } = useKnockFeed();
7 |
8 | const onNotificationsReceived = ({ items }) => {
9 | // Whenever we receive a new notification from our real-time stream, show a toast
10 | // (note here that we can receive > 1 items in a batch)
11 | console.log(items);
12 | items.forEach((notification) => {
13 | toast(
14 | `New view on ${notification.data.videoId} by ${notification.data.user}`,
15 | {
16 | id: notification.id,
17 | icon: '👏',
18 | }
19 | );
20 | });
21 |
22 | // Optionally, you may want to mark them as "seen" as well
23 | feedClient.markAsArchived(items);
24 | };
25 |
26 | useEffect(() => {
27 | // Receive all real-time notifications on our feed
28 | feedClient.on('items.received.realtime', onNotificationsReceived);
29 |
30 | // Cleanup
31 | return () =>
32 | feedClient.off('items.received.realtime', onNotificationsReceived);
33 | }, [feedClient]);
34 |
35 | return ;
36 | };
37 |
38 | export default FeedToasts;
39 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/app/components/SubscribeButton.tsx:
--------------------------------------------------------------------------------
1 | export default function SubscribeButton() {
2 | return (
3 |
4 | Subscribe
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/app/components/SubscriberCount.tsx:
--------------------------------------------------------------------------------
1 | export default function SubscriberCount() {
2 | return (
3 |
4 | 70K subscribers
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/app/components/player.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import MuxPlayer from '@mux/mux-player-react';
4 |
5 | export default function Player() {
6 | return (
7 |
11 | );
12 | }
13 |
14 | async function callOnPlay() {
15 | await fetch('/api/knock/on-play', {
16 | method: 'post',
17 | body: JSON.stringify({ videoId: 'test video', user: 'test user' }),
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/webhook-notifications-knock/app/favicon.ico
--------------------------------------------------------------------------------
/webhook-notifications-knock/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | @media (prefers-color-scheme: dark) {
12 | :root {
13 | --foreground-rgb: 255, 255, 255;
14 | --background-start-rgb: 0, 0, 0;
15 | --background-end-rgb: 0, 0, 0;
16 | }
17 | }
18 |
19 | body {
20 | color: rgb(var(--foreground-rgb));
21 | background: linear-gradient(
22 | to bottom,
23 | transparent,
24 | rgb(var(--background-end-rgb))
25 | )
26 | rgb(var(--background-start-rgb));
27 | }
28 |
29 | @layer utilities {
30 | .text-balance {
31 | text-wrap: balance;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next';
2 | import { Inter } from 'next/font/google';
3 | import './globals.css';
4 | import FeedContainer from './components/FeedContainer';
5 | import Link from 'next/link';
6 | import SubscribeButton from './components/SubscribeButton';
7 |
8 | const inter = Inter({ subsets: ['latin'] });
9 |
10 | export const metadata: Metadata = {
11 | title: 'Create Next App',
12 | description: 'Generated by create next app',
13 | };
14 |
15 | export default function RootLayout({
16 | children,
17 | }: Readonly<{
18 | children: React.ReactNode;
19 | }>) {
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | MyTube
29 |
30 |
31 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | {children}
46 |
47 |
48 |
49 | );
50 | }
51 |
52 | function YoutubeIcon(props: any) {
53 | return (
54 |
66 |
67 |
68 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/app/lib/knock.ts:
--------------------------------------------------------------------------------
1 | import { Knock } from '@knocklabs/node';
2 | const knockClient = new Knock(process.env.KNOCK_SECRET_KEY);
3 |
4 | export async function createChannel(channelName: string, props: Object) {
5 | await knockClient.objects.set('channels', channelName, props);
6 | return 'success';
7 | }
8 |
9 | export async function getSubscriptions(channelName: string) {
10 | const subscriptions = await knockClient.objects.getSubscriptions(
11 | 'channels',
12 | channelName
13 | );
14 | return subscriptions;
15 | }
16 |
17 | export async function subscribeToChannel(channelName: string, userId: string) {
18 | const subscription = await knockClient.objects.addSubscriptions(
19 | 'channels',
20 | channelName,
21 | { recipients: [userId] }
22 | );
23 |
24 | return subscription;
25 | }
26 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mux-knock-notifications",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@knocklabs/node": "^0.6.8",
13 | "@knocklabs/react": "^0.2.9",
14 | "@mux/mux-node": "^8.3.0",
15 | "@mux/mux-player-react": "^2.4.1",
16 | "next": "14.1.4",
17 | "react": "^18",
18 | "react-dom": "^18",
19 | "react-hot-toast": "^2.4.1"
20 | },
21 | "devDependencies": {
22 | "@types/node": "^20",
23 | "@types/react": "^18",
24 | "@types/react-dom": "^18",
25 | "autoprefixer": "^10.0.1",
26 | "eslint": "^8",
27 | "eslint-config-next": "14.1.4",
28 | "postcss": "^8",
29 | "tailwindcss": "^3.3.0",
30 | "typescript": "^5"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/public/placeholder-user.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muxinc/examples/b4ec828052003854aa6b3037f59bb655440b1f11/webhook-notifications-knock/public/placeholder-user.jpg
--------------------------------------------------------------------------------
/webhook-notifications-knock/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: [
5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
13 | "gradient-conic":
14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
15 | },
16 | },
17 | },
18 | plugins: [],
19 | };
20 | export default config;
21 |
--------------------------------------------------------------------------------
/webhook-notifications-knock/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": [
25 | "next-env.d.ts",
26 | "**/*.ts",
27 | "**/**/*.ts",
28 | "**/*.tsx",
29 | ".next/types/**/*.ts"
30 | ],
31 | "exclude": ["node_modules"]
32 | }
33 |
--------------------------------------------------------------------------------