├── .gitignore ├── .vscode └── settings.json ├── README.md ├── build ├── Dockerfile ├── build.sh ├── package.json └── stream.js ├── install-bbb-streaming.sh ├── owncast ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Earthfile ├── LICENSE ├── README.md ├── activitypub │ ├── activitypub.go │ ├── apmodels │ │ ├── activity.go │ │ ├── actor.go │ │ ├── actor_test.go │ │ ├── hashtag.go │ │ ├── inboxRequest.go │ │ ├── message.go │ │ ├── utils.go │ │ └── webfinger.go │ ├── controllers │ │ ├── actors.go │ │ ├── followers.go │ │ ├── inbox.go │ │ ├── nodeinfo.go │ │ ├── object.go │ │ ├── outbox.go │ │ └── webfinger.go │ ├── crypto │ │ ├── keys.go │ │ ├── publicKey.go │ │ └── sign.go │ ├── inbox │ │ ├── announce.go │ │ ├── chat.go │ │ ├── constants.go │ │ ├── create.go │ │ ├── follow.go │ │ ├── like.go │ │ ├── undo.go │ │ ├── update.go │ │ ├── worker.go │ │ ├── worker_test.go │ │ └── workerpool.go │ ├── outbox │ │ └── outbox.go │ ├── persistence │ │ ├── followers.go │ │ └── persistence.go │ ├── requests │ │ ├── acceptFollow.go │ │ └── http.go │ ├── resolvers │ │ ├── follow.go │ │ └── resolve.go │ ├── router.go │ ├── webfinger │ │ └── webfinger.go │ └── workerpool │ │ └── outbound.go ├── auth │ ├── auth.go │ ├── fediverse │ │ ├── fediverse.go │ │ └── fediverse_test.go │ ├── indieauth │ │ ├── client.go │ │ ├── helpers.go │ │ ├── random.go │ │ ├── request.go │ │ ├── response.go │ │ └── server.go │ └── persistence.go ├── build │ ├── .gitignore │ ├── admin │ │ └── bundleAdmin.sh │ ├── javascript │ │ ├── README.md │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── postcss.config.js │ │ └── tailwind.config.js │ └── release │ │ ├── build.sh │ │ ├── docker-nightly.sh │ │ └── docker-webv2.sh ├── config │ ├── config.go │ ├── configUtils.go │ ├── constants.go │ ├── defaults.go │ ├── updaterConfig_enabled.go │ └── verifyInstall.go ├── controllers │ ├── admin.go │ ├── admin │ │ ├── chat.go │ │ ├── config.go │ │ ├── connectedClients.go │ │ ├── disconnect.go │ │ ├── externalAPIUsers.go │ │ ├── federation.go │ │ ├── followers.go │ │ ├── hardware.go │ │ ├── index.go │ │ ├── logs.go │ │ ├── notifications.go │ │ ├── serverConfig.go │ │ ├── status.go │ │ ├── update.go │ │ ├── video.go │ │ ├── viewers.go │ │ ├── webhooks.go │ │ └── yp.go │ ├── auth │ │ ├── fediverse │ │ │ └── fediverse.go │ │ └── indieauth │ │ │ ├── client.go │ │ │ └── server.go │ ├── chat.go │ ├── config.go │ ├── constants.go │ ├── controllers.go │ ├── embed.go │ ├── emoji.go │ ├── followers.go │ ├── hls.go │ ├── index.go │ ├── logo.go │ ├── notifications.go │ ├── pagination.go │ ├── ping.go │ ├── playbackMetrics.go │ ├── remoteFollow.go │ ├── status.go │ └── video.go ├── core │ ├── chat │ │ ├── chat.go │ │ ├── chatclient.go │ │ ├── concurrentConnections.go │ │ ├── concurrentConnections_freebsd.go │ │ ├── concurrentConnections_windows.go │ │ ├── events.go │ │ ├── events │ │ │ ├── actionEvent.go │ │ │ ├── connectedClientInfo.go │ │ │ ├── events.go │ │ │ ├── eventtype.go │ │ │ ├── fediverseEngagementEvent.go │ │ │ ├── nameChangeEvent.go │ │ │ ├── setMessageVisibilityEvent.go │ │ │ ├── systemMessageEvent.go │ │ │ ├── userDisabledEvent.go │ │ │ ├── userJoinedEvent.go │ │ │ └── userMessageEvent.go │ │ ├── messageRendering_test.go │ │ ├── messages.go │ │ ├── persistence.go │ │ ├── pruner.go │ │ ├── server.go │ │ ├── utils.go │ │ └── utils_windows.go │ ├── core.go │ ├── data │ │ ├── activitypub.go │ │ ├── cache.go │ │ ├── config.go │ │ ├── configEntry.go │ │ ├── crypto.go │ │ ├── data.go │ │ ├── data_test.go │ │ ├── datastoreMigrations.go │ │ ├── defaults.go │ │ ├── messages.go │ │ ├── migrations.go │ │ ├── persistence.go │ │ ├── types.go │ │ ├── users.go │ │ ├── utils.go │ │ └── webhooks.go │ ├── offlineState.go │ ├── playlist │ │ └── writer.go │ ├── rtmp │ │ ├── broadcaster.go │ │ ├── rtmp.go │ │ ├── utils.go │ │ └── utils_test.go │ ├── stats.go │ ├── status.go │ ├── storage.go │ ├── storageproviders │ │ ├── local.go │ │ └── s3Storage.go │ ├── streamState.go │ ├── transcoder │ │ ├── codecs.go │ │ ├── fileWriterReceiverService.go │ │ ├── hlsFilesystemCleanup.go │ │ ├── hlsHandler.go │ │ ├── thumbnailGenerator.go │ │ ├── transcoder.go │ │ ├── transcoder_nvenc_test.go │ │ ├── transcoder_omx_test.go │ │ ├── transcoder_vaapi_test.go │ │ ├── transcoder_videotoolbox_test.go │ │ ├── transcoder_x264_test.go │ │ └── utils.go │ ├── user │ │ ├── externalAPIUser.go │ │ └── user.go │ └── webhooks │ │ ├── chat.go │ │ ├── chat_test.go │ │ ├── stream.go │ │ ├── stream_test.go │ │ ├── webhooks.go │ │ ├── webhooks_test.go │ │ └── workerpool.go ├── db │ ├── README.md │ ├── db.go │ ├── models.go │ ├── query.sql │ ├── query.sql.go │ └── schema.sql ├── docs │ ├── DESIGN.md │ ├── SECURITY.md │ ├── api │ │ └── index.html │ ├── dependencies.md │ └── product-definition.md ├── examples │ ├── owncast-sample.service │ └── owncast-systemd-service.md ├── geoip │ └── geoip.go ├── go.mod ├── go.sum ├── logging │ ├── logging.go │ └── paths.go ├── main.go ├── metrics │ ├── alerting.go │ ├── hardware.go │ ├── healthOverview.go │ ├── metrics.go │ ├── playback.go │ ├── prometheus.go │ ├── timestampedValue.go │ └── viewers.go ├── models │ ├── baseAPIResponse.go │ ├── broadcaster.go │ ├── client.go │ ├── currentBroadcast.go │ ├── emoji.go │ ├── eventType.go │ ├── externalAction.go │ ├── federatedActivity.go │ ├── follower.go │ ├── ipAddress.go │ ├── latencyLevels.go │ ├── notification.go │ ├── pingMessage.go │ ├── playlist.go │ ├── s3Storage.go │ ├── socialHandle.go │ ├── stats.go │ ├── status.go │ ├── storageProvider.go │ ├── streamHealth.go │ ├── streamOutputVariant.go │ ├── userJoinedEvent.go │ ├── viewer.go │ └── webhook.go ├── notifications │ ├── browser │ │ └── browser.go │ ├── channels.go │ ├── discord │ │ └── discord.go │ ├── notifications.go │ ├── persistence.go │ └── twitter │ │ └── twitter.go ├── openapi.yaml ├── renovate.json ├── router │ ├── middleware │ │ ├── activityPub.go │ │ ├── auth.go │ │ ├── caching.go │ │ ├── cors.go │ │ ├── headers.go │ │ └── pagination.go │ └── router.go ├── sqlc.yaml ├── static │ ├── admin │ │ ├── 404 │ │ │ └── index.html │ │ ├── _next │ │ │ └── static │ │ │ │ ├── IF0q45tHTUnbAhOX9pFCQ │ │ │ │ ├── _buildManifest.js │ │ │ │ ├── _middlewareManifest.js │ │ │ │ └── _ssgManifest.js │ │ │ │ ├── chunks │ │ │ │ ├── 1017-0760c7f39ffcc2a7.js │ │ │ │ ├── 1080-1a127ea7f5a8eb3d.js │ │ │ │ ├── 1371-f41477e42ee50603.js │ │ │ │ ├── 1556-f79a922e799c7a06.js │ │ │ │ ├── 1741-d9d648ade4ad86b9.js │ │ │ │ ├── 1758-3a8e1364ffda64ee.js │ │ │ │ ├── 1829-f5c4fb462b2f7e98.js │ │ │ │ ├── 1912.40e8274b3898c5b0.js │ │ │ │ ├── 2429-ccb4d7fa1648dd38.js │ │ │ │ ├── 2494-8114e9c6571377d1.js │ │ │ │ ├── 2589-9a64928383be2ad7.js │ │ │ │ ├── 29107295-4a69275373f23f88.js │ │ │ │ ├── 36bcf0ca-110fd889741d5f41.js │ │ │ │ ├── 4578-afc9eff4fbf5ecb1.js │ │ │ │ ├── 4763-7fd93797a527a971.js │ │ │ │ ├── 5473-623385148d67cba2.js │ │ │ │ ├── 5533-096cc7dc6703128f.js │ │ │ │ ├── 6003-f37682e25271f05f.js │ │ │ │ ├── 6132-0f911799dd6dd847.js │ │ │ │ ├── 6489-cea2e8971ed16ad4.js │ │ │ │ ├── 7751-48959ec0f11e9080.js │ │ │ │ ├── 7910-699eb8ed3467dc00.js │ │ │ │ ├── 8091-5bc21baa6d0d3232.js │ │ │ │ ├── 8879-af8bf87fdc518c08.js │ │ │ │ ├── framework-79bce4a3a540b080.js │ │ │ │ ├── main-c4d96150fb4f93a7.js │ │ │ │ ├── pages │ │ │ │ │ ├── _app-e57b2a440c783b12.js │ │ │ │ │ ├── _error-785557186902809b.js │ │ │ │ │ ├── access-tokens-d328b918d1f9b3d4.js │ │ │ │ │ ├── actions-9278698db4cd1a16.js │ │ │ │ │ ├── chat │ │ │ │ │ │ ├── messages-0df125d8b9455827.js │ │ │ │ │ │ └── users-c3f6235e6932151e.js │ │ │ │ │ ├── config-chat-bacb12d23264144b.js │ │ │ │ │ ├── config-federation-ea0f018fb4193b61.js │ │ │ │ │ ├── config-notify-10a8844dc11ca4b2.js │ │ │ │ │ ├── config-public-details-94ff52653eb2586e.js │ │ │ │ │ ├── config-server-details-cd516688accb84d3.js │ │ │ │ │ ├── config-social-items-42e2ed4eed8d4dd2.js │ │ │ │ │ ├── config-storage-5ff120c715bfdb04.js │ │ │ │ │ ├── config-video-32d86e0ba98dc6fe.js │ │ │ │ │ ├── federation │ │ │ │ │ │ ├── actions-7cfffddef3b58d86.js │ │ │ │ │ │ └── followers-d2d105c342c79f98.js │ │ │ │ │ ├── hardware-info-4723b20a84e4f461.js │ │ │ │ │ ├── help-deeb1c0f667c7d75.js │ │ │ │ │ ├── index-e0ac83ceaf99b5f0.js │ │ │ │ │ ├── logs-df4b23b85b8ac818.js │ │ │ │ │ ├── stream-health-4a811c8adeb950de.js │ │ │ │ │ ├── upgrade-1dcaa9f0a72f02f8.js │ │ │ │ │ ├── viewer-info-03fcbea265510389.js │ │ │ │ │ └── webhooks-651cb241952e0e4a.js │ │ │ │ ├── polyfills-5cd94c89d3acac5f.js │ │ │ │ └── webpack-da6d6ade1d5643c2.js │ │ │ │ └── css │ │ │ │ ├── 7e98ab6b3b660d68.css │ │ │ │ └── e773f9ad06a56dc3.css │ │ ├── access-tokens │ │ │ └── index.html │ │ ├── actions │ │ │ └── index.html │ │ ├── chat │ │ │ ├── messages │ │ │ │ └── index.html │ │ │ └── users │ │ │ │ └── index.html │ │ ├── config-chat │ │ │ └── index.html │ │ ├── config-federation │ │ │ └── index.html │ │ ├── config-notify │ │ │ └── index.html │ │ ├── config-public-details │ │ │ └── index.html │ │ ├── config-server-details │ │ │ └── index.html │ │ ├── config-social-items │ │ │ └── index.html │ │ ├── config-storage │ │ │ └── index.html │ │ ├── config-video │ │ │ └── index.html │ │ ├── federation │ │ │ ├── actions │ │ │ │ └── index.html │ │ │ └── followers │ │ │ │ └── index.html │ │ ├── fediverse-white.png │ │ ├── hardware-info │ │ │ └── index.html │ │ ├── help │ │ │ └── index.html │ │ ├── index.html │ │ ├── logs │ │ │ └── index.html │ │ ├── stream-health │ │ │ └── index.html │ │ ├── upgrade │ │ │ └── index.html │ │ ├── viewer-info │ │ │ └── index.html │ │ └── webhooks │ │ │ └── index.html │ ├── golive.html.tmpl │ ├── metadata.html.tmpl │ ├── offline.ts │ └── static.go ├── test │ ├── .gitignore │ ├── README.md │ ├── automated │ │ ├── .gitignore │ │ ├── api │ │ │ ├── admin.test.js │ │ │ ├── chat.test.js │ │ │ ├── chatmoderation.test.js │ │ │ ├── chatusers.test.js │ │ │ ├── configmanagement.test.js │ │ │ ├── index.test.js │ │ │ ├── integrations.test.js │ │ │ ├── lib │ │ │ │ └── chat.js │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── run.sh │ │ ├── browser │ │ │ ├── README.md │ │ │ ├── admin.test.js │ │ │ ├── bot-share-search-scrapers.test.js │ │ │ ├── chat-embed.test.js │ │ │ ├── jest.config.json │ │ │ ├── lib │ │ │ │ └── errors.js │ │ │ ├── main.test.js │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ ├── run.sh │ │ │ ├── screenshots │ │ │ │ └── .gitkeep │ │ │ ├── tests │ │ │ │ ├── chat.js │ │ │ │ └── video.js │ │ │ └── video-embed.test.js │ │ ├── hls │ │ │ ├── hls.test.js │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── run.sh │ │ └── test.mp4 │ ├── fakeChat.js │ ├── load │ │ ├── README.md │ │ ├── badchatdata.js │ │ ├── chatLoadTest.js │ │ ├── hls.yaml │ │ ├── httpGetTest.yaml │ │ ├── package-lock.json │ │ └── package.json │ ├── ocTestStream.sh │ ├── package-lock.json │ ├── package.json │ └── userColorsTest.js ├── utils │ ├── accessTokens.go │ ├── accessTokens_test.go │ ├── backup.go │ ├── clientId.go │ ├── nulltime.go │ ├── performanceTimer.go │ ├── phraseGenerator.go │ ├── restendpointhelper.go │ ├── restendpointhelper_test.go │ ├── utils.go │ └── utils_test.go ├── webroot │ ├── favicon.ico │ ├── img │ │ ├── airplay.png │ │ ├── authenticated.svg │ │ ├── ban-user-grey.svg │ │ ├── ban-user.svg │ │ ├── bot.svg │ │ ├── browser-push-notifications-settings.png │ │ ├── emoji │ │ │ ├── Reaper-gg.png │ │ │ ├── Reaper-hi.png │ │ │ ├── Reaper-hype.png │ │ │ ├── Reaper-lol.png │ │ │ ├── Reaper-love.png │ │ │ ├── Reaper-rage.png │ │ │ ├── Reaper-rip.png │ │ │ ├── Reaper-wtf.png │ │ │ ├── ac-box.png │ │ │ ├── ac-construction.png │ │ │ ├── ac-fossil.png │ │ │ ├── ac-item-leaf.png │ │ │ ├── ac-kkslider.png │ │ │ ├── ac-moneytree.png │ │ │ ├── ac-mosquito.png │ │ │ ├── ac-shirt.png │ │ │ ├── ac-song.png │ │ │ ├── ac-tree.png │ │ │ ├── ac-turnip.png │ │ │ ├── ac-weeds.png │ │ │ ├── alert.gif │ │ │ ├── bananadance.gif │ │ │ ├── bb8.png │ │ │ ├── beerparrot.gif │ │ │ ├── bells.png │ │ │ ├── birthdaypartyparrot.gif │ │ │ ├── blacklightsaber.png │ │ │ ├── bluelightsaber.png │ │ │ ├── bluntparrot.gif │ │ │ ├── bobaparrot.gif │ │ │ ├── cakeparrot.gif │ │ │ ├── chewbacca.png │ │ │ ├── chillparrot.gif │ │ │ ├── christmasparrot.gif │ │ │ ├── coffeeparrot.gif │ │ │ ├── confusedparrot.gif │ │ │ ├── copparrot.gif │ │ │ ├── coronavirus.png │ │ │ ├── covid19parrot.gif │ │ │ ├── cryptoparrot.gif │ │ │ ├── dabparrot.gif │ │ │ ├── dadparrot.gif │ │ │ ├── daftpunkparrot.gif │ │ │ ├── darkbeerparrot.gif │ │ │ ├── darkmodeparrot.gif │ │ │ ├── darth_vader.png │ │ │ ├── dealwithitparrot.gif │ │ │ ├── death_star.png │ │ │ ├── discoparrot.gif │ │ │ ├── division-gg.png │ │ │ ├── division-hi.png │ │ │ ├── division-hype.png │ │ │ ├── division-lol.png │ │ │ ├── division-omg.png │ │ │ ├── division-rage.png │ │ │ ├── division-rip.png │ │ │ ├── division-wtf.png │ │ │ ├── docparrot.gif │ │ │ ├── donutparrot.gif │ │ │ ├── doom_mad.gif │ │ │ ├── empire.png │ │ │ ├── everythingsfineparrot.gif │ │ │ ├── evilparrot.gif │ │ │ ├── explodyparrot.gif │ │ │ ├── fixparrot.gif │ │ │ ├── flyingmoneyparrot.gif │ │ │ ├── footballparrot.gif │ │ │ ├── gabe1.png │ │ │ ├── gabe2.png │ │ │ ├── gentlemanparrot.gif │ │ │ ├── githubparrot.gif │ │ │ ├── goomba.gif │ │ │ ├── gothparrot.gif │ │ │ ├── hamburgerparrot.gif │ │ │ ├── harrypotterparrot.gif │ │ │ ├── headbangingparrot.gif │ │ │ ├── headingparrot.gif │ │ │ ├── headsetparrot.gif │ │ │ ├── hmmparrot.gif │ │ │ ├── hypnoparrot.gif │ │ │ ├── icecreamparrot.gif │ │ │ ├── illuminatiparrot.gif │ │ │ ├── jediparrot.gif │ │ │ ├── keanu_thanks.gif │ │ │ ├── laptop_parrot.gif │ │ │ ├── loveparrot.gif │ │ │ ├── mandalorian.png │ │ │ ├── margaritaparrot.gif │ │ │ ├── mario.gif │ │ │ ├── matrixparrot.gif │ │ │ ├── meldparrot.gif │ │ │ ├── metalparrot.gif │ │ │ ├── michaeljacksonparrot.gif │ │ │ ├── moonparrot.gif │ │ │ ├── moonwalkingparrot.gif │ │ │ ├── mustacheparrot.gif │ │ │ ├── nicolas_cage_party.gif │ │ │ ├── nodeparrot.gif │ │ │ ├── norwegianblueparrot.gif │ │ │ ├── opensourceparrot.gif │ │ │ ├── originalparrot.gif │ │ │ ├── owncast.png │ │ │ ├── palpatine.png │ │ │ ├── papalparrot.gif │ │ │ ├── parrot.gif │ │ │ ├── parrotnotfound.gif │ │ │ ├── partyparrot.gif │ │ │ ├── phparrot.gif │ │ │ ├── pirateparrot.gif │ │ │ ├── pizzaparrot.gif │ │ │ ├── pokeparrot.gif │ │ │ ├── popcornparrot.gif │ │ │ ├── porg.png │ │ │ ├── portalparrot.gif │ │ │ ├── pumpkinparrot.gif │ │ │ ├── quadparrot.gif │ │ │ ├── r2d2.png │ │ │ ├── redenvelopeparrot.gif │ │ │ ├── ripparrot.gif │ │ │ ├── rotatingparrot.gif │ │ │ ├── ryangoslingparrot.gif │ │ │ ├── rythmicalparrot.gif │ │ │ ├── sadparrot.gif │ │ │ ├── schnitzelparrot.gif │ │ │ ├── scienceparrot.gif │ │ │ ├── shipitparrot.gif │ │ │ ├── shufflepartyparrot.gif │ │ │ ├── sintparrot.gif │ │ │ ├── sithparrot.gif │ │ │ ├── skiparrot.gif │ │ │ ├── sleepingparrot.gif │ │ │ ├── sonic.gif │ │ │ ├── spyparrot.gif │ │ │ ├── stalkerparrot.gif │ │ │ ├── starwars.png │ │ │ ├── stayhomeparrot.gif │ │ │ ├── storm_trooper.gif │ │ │ ├── stormtrooper.png │ │ │ ├── sushiparrot.gif │ │ │ ├── tacoparrot.gif │ │ │ ├── tennisparrot.gif │ │ │ ├── thanks.png │ │ │ ├── thumbsupparrot.gif │ │ │ ├── tiedyeparrot.gif │ │ │ ├── tpparrot.gif │ │ │ ├── transparront.gif │ │ │ ├── twinsparrot.gif │ │ │ ├── upvoteparrot.gif │ │ │ ├── vikingparrot.gif │ │ │ ├── wesmart.png │ │ │ ├── wfhparrot.gif │ │ │ ├── wineparrot.gif │ │ │ └── yoda.gif │ │ ├── favicon │ │ │ ├── android-icon-144x144.png │ │ │ ├── android-icon-192x192.png │ │ │ ├── android-icon-36x36.png │ │ │ ├── android-icon-48x48.png │ │ │ ├── android-icon-72x72.png │ │ │ ├── android-icon-96x96.png │ │ │ ├── apple-icon-114x114.png │ │ │ ├── apple-icon-120x120.png │ │ │ ├── apple-icon-144x144.png │ │ │ ├── apple-icon-152x152.png │ │ │ ├── apple-icon-180x180.png │ │ │ ├── apple-icon-57x57.png │ │ │ ├── apple-icon-60x60.png │ │ │ ├── apple-icon-72x72.png │ │ │ ├── apple-icon-76x76.png │ │ │ ├── apple-icon-precomposed.png │ │ │ ├── apple-icon.png │ │ │ ├── browserconfig.xml │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon-96x96.png │ │ │ ├── ms-icon-144x144.png │ │ │ ├── ms-icon-150x150.png │ │ │ ├── ms-icon-310x310.png │ │ │ └── ms-icon-70x70.png │ │ ├── fediverse-black.png │ │ ├── fediverse-color.png │ │ ├── fediverse-white.png │ │ ├── follow.svg │ │ ├── hide-message-grey.svg │ │ ├── hide-message.svg │ │ ├── indieauth.png │ │ ├── like.svg │ │ ├── loading.gif │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── menu-filled.svg │ │ ├── menu-vert.svg │ │ ├── menu.svg │ │ ├── moderator-grey.svg │ │ ├── moderator-nobackground.svg │ │ ├── moderator.svg │ │ ├── notification-bell.svg │ │ ├── owncast-background.png │ │ ├── platformlogos │ │ │ ├── bandcamp.svg │ │ │ ├── default.svg │ │ │ ├── discord.svg │ │ │ ├── donate.svg │ │ │ ├── facebook.svg │ │ │ ├── fediverse.svg │ │ │ ├── follow.svg │ │ │ ├── github.svg │ │ │ ├── gitlab.svg │ │ │ ├── google.svg │ │ │ ├── instagram.svg │ │ │ ├── keyoxide.png │ │ │ ├── ko-fi.svg │ │ │ ├── lbry.svg │ │ │ ├── liberapay.svg │ │ │ ├── link.svg │ │ │ ├── linkedin.svg │ │ │ ├── mastodon.svg │ │ │ ├── matrix.svg │ │ │ ├── odysee.svg │ │ │ ├── patreon.svg │ │ │ ├── paypal.svg │ │ │ ├── snapchat.svg │ │ │ ├── soundcloud.svg │ │ │ ├── spotify.svg │ │ │ ├── steam.svg │ │ │ ├── tiktok.svg │ │ │ ├── twitch.svg │ │ │ ├── twitter.svg │ │ │ ├── xmpp.svg │ │ │ └── youtube.svg │ │ ├── repost.svg │ │ ├── smiley.png │ │ ├── user-icon.svg │ │ └── user-settings.svg │ ├── index-standalone-chat-readonly.html │ ├── index-standalone-chat-readwrite.html │ ├── index-standalone-chat.html │ ├── index-video-only.html │ ├── index.html │ ├── js │ │ ├── app-standalone-chat.js │ │ ├── app-video-only.js │ │ ├── app.js │ │ ├── chat │ │ │ └── register.js │ │ ├── components │ │ │ ├── auth-fediverse.js │ │ │ ├── auth-indieauth.js │ │ │ ├── auth-modal.js │ │ │ ├── chat-settings-modal.js │ │ │ ├── chat │ │ │ │ ├── chat-input.js │ │ │ │ ├── chat-menu.js │ │ │ │ ├── chat-message-view.js │ │ │ │ ├── chat.js │ │ │ │ ├── content-editable.js │ │ │ │ ├── message.js │ │ │ │ ├── moderator-actions.js │ │ │ │ └── username.js │ │ │ ├── external-action-modal.js │ │ │ ├── federation │ │ │ │ └── followers.js │ │ │ ├── fediverse-follow-modal.js │ │ │ ├── icons │ │ │ │ ├── AuthIcon.js │ │ │ │ ├── CaretDownIcon.js │ │ │ │ ├── ChatIcon.js │ │ │ │ ├── CheckIcon.js │ │ │ │ ├── CloseIcon.js │ │ │ │ ├── EditIcon.js │ │ │ │ ├── UserIcon.js │ │ │ │ └── index.js │ │ │ ├── latencyCompensator.js │ │ │ ├── notification.js │ │ │ ├── platform-logos-list.js │ │ │ ├── player.js │ │ │ ├── tab-bar.js │ │ │ └── video-poster.js │ │ ├── metrics │ │ │ └── playback.js │ │ ├── notification │ │ │ └── registerWeb.js │ │ ├── utils │ │ │ ├── chat.js │ │ │ ├── constants.js │ │ │ ├── helpers.js │ │ │ ├── user-colors.js │ │ │ └── websocket.js │ │ └── web_modules │ │ │ ├── @joeattardi │ │ │ └── emoji-button.js │ │ │ ├── @videojs │ │ │ └── themes │ │ │ │ └── fantasy │ │ │ │ └── index.css │ │ │ ├── common │ │ │ └── _commonjsHelpers-8c19dec8.js │ │ │ ├── htm.js │ │ │ ├── import-map.json │ │ │ ├── markjs │ │ │ └── dist │ │ │ │ └── mark.es6.min.js │ │ │ ├── micromodal │ │ │ └── dist │ │ │ │ └── micromodal.min.js │ │ │ ├── preact.js │ │ │ ├── preact │ │ │ └── hooks.js │ │ │ ├── tailwindcss │ │ │ └── dist │ │ │ │ └── tailwind.min.css │ │ │ └── videojs │ │ │ ├── dist │ │ │ ├── video-js.min.css │ │ │ └── video.min.js │ │ │ └── video-js.min.css │ ├── manifest.json │ ├── serviceWorker.js │ └── styles │ │ ├── app.css │ │ ├── chat.css │ │ ├── standalone-chat-readonly.css │ │ ├── standalone-chat-readwrite.css │ │ ├── user-content.css │ │ ├── video-only.css │ │ └── video.css └── yp │ ├── README.md │ ├── api.go │ └── yp.go ├── restart-streaming.sh ├── sample-env ├── start-streaming.sh └── stop-streaming.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .ds_store 3 | *.yml 4 | *.code-workspace 5 | *.iml 6 | test.sh 7 | startFfmpeg.sh 8 | env 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/usr/bin/python3" 3 | } -------------------------------------------------------------------------------- /build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-bullseye-slim 2 | 3 | 4 | RUN apt-get update && apt-get install curl gnupg psmisc wget -y \ 5 | && curl --location --silent https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ 6 | && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ 7 | && apt-get update \ 8 | && apt-get install google-chrome-stable xvfb ffmpeg -y \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | RUN echo ""> /etc/apt/sources.list.d/google.list 12 | 13 | 14 | RUN apt update -y && apt remove google-chrome-stable -y && wget --no-verbose -O /tmp/chrome.deb http://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_104.0.5112.101-1_amd64.deb \ 15 | && apt install -y /tmp/chrome.deb \ 16 | && rm /tmp/chrome.deb 17 | 18 | WORKDIR /usr/src/app 19 | RUN rm -rf node_modules 20 | 21 | COPY package.* . 22 | RUN npm install 23 | COPY stream.js ./stream.js 24 | 25 | CMD ["node","stream.js" ] 26 | 27 | -------------------------------------------------------------------------------- /build/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker build -t $@ . -------------------------------------------------------------------------------- /build/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bbb-streaming", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "bigbluebutton-js": "^0.2.0", 13 | "dotenv": "^8.6.0", 14 | "express": "^4.17.1", 15 | "fs-extra": "^10.0.0", 16 | "puppeteer-extra-plugin-stealth": "^2.11.1", 17 | "puppeteer-stream": "^2.1.1", 18 | "xvfb": "^0.4.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /install-bbb-streaming.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #Generating enf file 4 | cp sample-env env 5 | 6 | #Pulling streaming image 7 | docker pull manishkatyan/bbb-streaming:v2.5.1 8 | -------------------------------------------------------------------------------- /owncast/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Help contribute to Owncast! 2 | 3 | Owncast is a growing open source project that is giving freedom, flexibility and fun to live streamers. 4 | And while we have a small team of kind, talented and thoughtful volunteers, we have gaps in our skillset that we’d love to fill so we can get even better at building tools that make a difference for people. 5 | 6 | We abide by our [Code of Conduct](https://owncast.online/contribute/) and feel strongly about open, appreciative, and empathetic people joining us. 7 | We’ve been very lucky to have this so far, so maybe you can help us with your skills and passion, too! 8 | 9 | There is a larger, more detailed, and more up-to-date [guide for helping contribute to Owncast on our website](https://owncast.online/help/). 10 | -------------------------------------------------------------------------------- /owncast/Dockerfile: -------------------------------------------------------------------------------- 1 | # Perform a build 2 | FROM golang:alpine AS build 3 | RUN mkdir /build 4 | ADD . /build 5 | WORKDIR /build 6 | RUN apk update && apk add --no-cache git gcc build-base linux-headers ffmpeg ffmpeg-libs ca-certificates && update-ca-certificates 7 | 8 | ARG VERSION=dev 9 | ENV VERSION=${VERSION} 10 | ARG GIT_COMMIT 11 | ENV GIT_COMMIT=${GIT_COMMIT} 12 | ARG NAME=docker 13 | ENV NAME=${NAME} 14 | 15 | ENTRYPOINT ["go", "run", "main.go"] 16 | EXPOSE 8080 1935 -------------------------------------------------------------------------------- /owncast/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gabe Kangas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /owncast/activitypub/apmodels/hashtag.go: -------------------------------------------------------------------------------- 1 | package apmodels 2 | 3 | import ( 4 | "net/url" 5 | 6 | "github.com/go-fed/activity/streams" 7 | "github.com/go-fed/activity/streams/vocab" 8 | ) 9 | 10 | // MakeHashtag will create and return a mastodon toot hashtag object with the provided name. 11 | func MakeHashtag(name string) vocab.TootHashtag { 12 | u, _ := url.Parse("https://directory.owncast.online/tags/" + name) 13 | 14 | hashtag := streams.NewTootHashtag() 15 | hashtagName := streams.NewActivityStreamsNameProperty() 16 | hashtagName.AppendXMLSchemaString("#" + name) 17 | hashtag.SetActivityStreamsName(hashtagName) 18 | 19 | hashtagHref := streams.NewActivityStreamsHrefProperty() 20 | hashtagHref.Set(u) 21 | hashtag.SetActivityStreamsHref(hashtagHref) 22 | 23 | return hashtag 24 | } 25 | -------------------------------------------------------------------------------- /owncast/activitypub/apmodels/inboxRequest.go: -------------------------------------------------------------------------------- 1 | package apmodels 2 | 3 | import "net/http" 4 | 5 | // InboxRequest represents an inbound request to the ActivityPub inbox. 6 | type InboxRequest struct { 7 | Request *http.Request 8 | ForLocalAccount string 9 | Body []byte 10 | } 11 | -------------------------------------------------------------------------------- /owncast/activitypub/controllers/object.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/owncast/owncast/activitypub/apmodels" 8 | "github.com/owncast/owncast/activitypub/crypto" 9 | "github.com/owncast/owncast/activitypub/persistence" 10 | "github.com/owncast/owncast/activitypub/requests" 11 | "github.com/owncast/owncast/core/data" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // ObjectHandler handles requests for a single federated ActivityPub object. 16 | func ObjectHandler(w http.ResponseWriter, r *http.Request) { 17 | if !data.GetFederationEnabled() { 18 | w.WriteHeader(http.StatusMethodNotAllowed) 19 | return 20 | } 21 | 22 | // If private federation mode is enabled do not allow access to objects. 23 | if data.GetFederationIsPrivate() { 24 | w.WriteHeader(http.StatusNotFound) 25 | return 26 | } 27 | 28 | iri := strings.Join([]string{strings.TrimSuffix(data.GetServerURL(), "/"), r.URL.Path}, "") 29 | object, _, _, err := persistence.GetObjectByIRI(iri) 30 | if err != nil { 31 | w.WriteHeader(http.StatusNotFound) 32 | return 33 | } 34 | 35 | accountName := data.GetDefaultFederationUsername() 36 | actorIRI := apmodels.MakeLocalIRIForAccount(accountName) 37 | publicKey := crypto.GetPublicKey(actorIRI) 38 | 39 | if err := requests.WriteResponse([]byte(object), w, publicKey); err != nil { 40 | log.Errorln(err) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /owncast/activitypub/crypto/publicKey.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import "net/url" 4 | 5 | // PublicKey represents a public key with associated ownership. 6 | type PublicKey struct { 7 | ID *url.URL `json:"id"` 8 | Owner *url.URL `json:"owner"` 9 | PublicKeyPem string `json:"publicKeyPem"` 10 | } 11 | -------------------------------------------------------------------------------- /owncast/activitypub/inbox/constants.go: -------------------------------------------------------------------------------- 1 | package inbox 2 | 3 | import "time" 4 | 5 | const ( 6 | maxAgeForEngagement = time.Hour * 36 7 | ) 8 | -------------------------------------------------------------------------------- /owncast/activitypub/inbox/create.go: -------------------------------------------------------------------------------- 1 | package inbox 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-fed/activity/streams/vocab" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | func handleCreateRequest(c context.Context, activity vocab.ActivityStreamsCreate) error { 11 | iri := activity.GetJSONLDId().GetIRI().String() 12 | return errors.New("not handling create request of: " + iri) 13 | } 14 | -------------------------------------------------------------------------------- /owncast/activitypub/inbox/undo.go: -------------------------------------------------------------------------------- 1 | package inbox 2 | 3 | import ( 4 | "context" 5 | 6 | log "github.com/sirupsen/logrus" 7 | 8 | "github.com/go-fed/activity/streams/vocab" 9 | ) 10 | 11 | func handleUndoInboxRequest(c context.Context, activity vocab.ActivityStreamsUndo) error { 12 | // Determine if this is an undo of a follow, favorite, announce, etc. 13 | o := activity.GetActivityStreamsObject() 14 | for iter := o.Begin(); iter != o.End(); iter = iter.Next() { 15 | if iter.IsActivityStreamsFollow() { 16 | // This is an Unfollow request 17 | if err := handleUnfollowRequest(c, activity); err != nil { 18 | return err 19 | } 20 | } else { 21 | t := iter.GetType() 22 | if t != nil { 23 | log.Traceln("Undo", t.GetTypeName(), "ignored") 24 | } else { 25 | log.Traceln("Undo (no type) ignored") 26 | } 27 | return nil 28 | } 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /owncast/activitypub/inbox/update.go: -------------------------------------------------------------------------------- 1 | package inbox 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-fed/activity/streams/vocab" 7 | "github.com/owncast/owncast/activitypub/persistence" 8 | "github.com/owncast/owncast/activitypub/resolvers" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func handleUpdateRequest(c context.Context, activity vocab.ActivityStreamsUpdate) error { 13 | // We only care about update events to followers. 14 | if !activity.GetActivityStreamsObject().At(0).IsActivityStreamsPerson() { 15 | return nil 16 | } 17 | 18 | actor, err := resolvers.GetResolvedActorFromActorProperty(activity.GetActivityStreamsActor()) 19 | if err != nil { 20 | log.Errorln(err) 21 | return err 22 | } 23 | 24 | return persistence.UpdateFollower(actor.ActorIri.String(), actor.Inbox.String(), actor.Name, actor.FullUsername, actor.Image.String()) 25 | } 26 | -------------------------------------------------------------------------------- /owncast/activitypub/inbox/workerpool.go: -------------------------------------------------------------------------------- 1 | package inbox 2 | 3 | import ( 4 | "github.com/owncast/owncast/activitypub/apmodels" 5 | log "github.com/sirupsen/logrus" 6 | ) 7 | 8 | const ( 9 | // InboxWorkerPoolSize defines the number of concurrent ActivityPub handlers. 10 | InboxWorkerPoolSize = 10 11 | ) 12 | 13 | // Job struct bundling the ActivityPub and the payload in one struct. 14 | type Job struct { 15 | request apmodels.InboxRequest 16 | } 17 | 18 | var queue chan Job 19 | 20 | // InitInboxWorkerPool starts n go routines that await ActivityPub jobs. 21 | func InitInboxWorkerPool() { 22 | queue = make(chan Job) 23 | 24 | // start workers 25 | for i := 1; i <= InboxWorkerPoolSize; i++ { 26 | go worker(i, queue) 27 | } 28 | } 29 | 30 | // AddToQueue will queue up an outbound http request. 31 | func AddToQueue(req apmodels.InboxRequest) { 32 | log.Tracef("Queued request for ActivityPub inbox handler") 33 | queue <- Job{req} 34 | } 35 | 36 | func worker(workerID int, queue <-chan Job) { 37 | log.Debugf("Started ActivityPub worker %d", workerID) 38 | 39 | for job := range queue { 40 | handle(job.request) 41 | 42 | log.Tracef("Done with ActivityPub inbox handler using worker %d", workerID) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /owncast/activitypub/router.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/owncast/owncast/activitypub/controllers" 7 | "github.com/owncast/owncast/router/middleware" 8 | ) 9 | 10 | // StartRouter will start the federation specific http router. 11 | func StartRouter() { 12 | // WebFinger 13 | http.HandleFunc("/.well-known/webfinger", controllers.WebfingerHandler) 14 | 15 | // Host Metadata 16 | http.HandleFunc("/.well-known/host-meta", controllers.HostMetaController) 17 | 18 | // Nodeinfo v1 19 | http.HandleFunc("/.well-known/nodeinfo", controllers.NodeInfoController) 20 | 21 | // x-nodeinfo v2 22 | http.HandleFunc("/.well-known/x-nodeinfo2", controllers.XNodeInfo2Controller) 23 | 24 | // Nodeinfo v2 25 | http.HandleFunc("/nodeinfo/2.0", controllers.NodeInfoV2Controller) 26 | 27 | // Instance details 28 | http.HandleFunc("/api/v1/instance", controllers.InstanceV1Controller) 29 | 30 | // Single ActivityPub Actor 31 | http.HandleFunc("/federation/user/", middleware.RequireActivityPubOrRedirect(controllers.ActorHandler)) 32 | 33 | // Single AP object 34 | http.HandleFunc("/federation/", middleware.RequireActivityPubOrRedirect(controllers.ObjectHandler)) 35 | } 36 | -------------------------------------------------------------------------------- /owncast/activitypub/webfinger/webfinger.go: -------------------------------------------------------------------------------- 1 | package webfinger 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | ) 10 | 11 | // GetWebfingerLinks will return webfinger data for an account. 12 | func GetWebfingerLinks(account string) ([]map[string]interface{}, error) { 13 | type webfingerResponse struct { 14 | Links []map[string]interface{} `json:"links"` 15 | } 16 | 17 | account = strings.TrimLeft(account, "@") // remove any leading @ 18 | accountComponents := strings.Split(account, "@") 19 | fediverseServer := accountComponents[1] 20 | 21 | // HTTPS is required. 22 | requestURL, err := url.Parse("https://" + fediverseServer) 23 | if err != nil { 24 | return nil, fmt.Errorf("unable to parse fediverse server host %s", fediverseServer) 25 | } 26 | 27 | requestURL.Path = "/.well-known/webfinger" 28 | query := requestURL.Query() 29 | query.Add("resource", fmt.Sprintf("acct:%s", account)) 30 | requestURL.RawQuery = query.Encode() 31 | 32 | response, err := http.DefaultClient.Get(requestURL.String()) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | defer response.Body.Close() 38 | 39 | var links webfingerResponse 40 | decoder := json.NewDecoder(response.Body) 41 | if err := decoder.Decode(&links); err != nil { 42 | return nil, err 43 | } 44 | 45 | return links.Links, nil 46 | } 47 | -------------------------------------------------------------------------------- /owncast/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | // Type represents a form of authentication. 4 | type Type string 5 | 6 | // The different auth types we support. 7 | const ( 8 | // IndieAuth https://indieauth.spec.indieweb.org/. 9 | IndieAuth Type = "indieauth" 10 | Fediverse Type = "fediverse" 11 | ) 12 | -------------------------------------------------------------------------------- /owncast/auth/indieauth/random.go: -------------------------------------------------------------------------------- 1 | package indieauth 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | "unsafe" 7 | ) 8 | 9 | const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 10 | const ( 11 | letterIdxBits = 6 // 6 bits to represent a letter index 12 | letterIdxMask = 1<= 0; { 22 | if remain == 0 { 23 | cache, remain = src.Int63(), letterIdxMax 24 | } 25 | if idx := int(cache & letterIdxMask); idx < len(letterBytes) { 26 | b[i] = letterBytes[idx] 27 | i-- 28 | } 29 | cache >>= letterIdxBits 30 | remain-- 31 | } 32 | 33 | return *(*string)(unsafe.Pointer(&b)) // nolint:gosec 34 | } 35 | -------------------------------------------------------------------------------- /owncast/auth/indieauth/request.go: -------------------------------------------------------------------------------- 1 | package indieauth 2 | 3 | import "net/url" 4 | 5 | // Request represents a single in-flight IndieAuth request. 6 | type Request struct { 7 | UserID string 8 | DisplayName string 9 | CurrentAccessToken string 10 | Endpoint *url.URL 11 | Redirect *url.URL // Outbound redirect URL to continue auth flow 12 | Callback *url.URL // Inbound URL to get auth flow results 13 | ClientID string 14 | CodeVerifier string 15 | CodeChallenge string 16 | State string 17 | Me *url.URL 18 | } 19 | -------------------------------------------------------------------------------- /owncast/auth/indieauth/response.go: -------------------------------------------------------------------------------- 1 | package indieauth 2 | 3 | // Profile represents optional profile data that is returned 4 | // when completing the IndieAuth flow. 5 | type Profile struct { 6 | Name string `json:"name"` 7 | URL string `json:"url"` 8 | Photo string `json:"photo"` 9 | } 10 | 11 | // Response the response returned when completing 12 | // the IndieAuth flow. 13 | type Response struct { 14 | Me string `json:"me,omitempty"` 15 | Profile Profile `json:"profile,omitempty"` 16 | Error string `json:"error,omitempty"` 17 | ErrorDescription string `json:"error_description,omitempty"` 18 | } 19 | -------------------------------------------------------------------------------- /owncast/build/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | web_modules -------------------------------------------------------------------------------- /owncast/build/admin/bundleAdmin.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2059 3 | 4 | set -o errexit 5 | set -o nounset 6 | set -o pipefail 7 | 8 | INSTALL_TEMP_DIRECTORY="$(mktemp -d)" 9 | PROJECT_SOURCE_DIR=$(pwd) 10 | cd $INSTALL_TEMP_DIRECTORY 11 | 12 | shutdown () { 13 | rm -rf "$INSTALL_TEMP_DIRECTORY" 14 | } 15 | trap shutdown INT TERM ABRT EXIT 16 | 17 | echo "Cloning owncast admin into $INSTALL_TEMP_DIRECTORY..." 18 | git clone https://github.com/owncast/owncast-admin 2> /dev/null 19 | cd owncast-admin 20 | 21 | echo "Installing npm modules for the owncast admin..." 22 | npm --silent install 2> /dev/null 23 | 24 | echo "Building owncast admin..." 25 | rm -rf .next 26 | (node_modules/.bin/next build && node_modules/.bin/next export) | grep info 27 | 28 | echo "Copying admin to project directory..." 29 | ADMIN_BUILD_DIR=$(pwd) 30 | cd $PROJECT_SOURCE_DIR 31 | mkdir -p admin 2> /dev/null 32 | cd admin 33 | 34 | # Remove the old one 35 | rm -rf $PROJECT_SOURCE_DIR/static/admin 36 | 37 | # Copy over the new one 38 | mv ${ADMIN_BUILD_DIR}/out $PROJECT_SOURCE_DIR/static/admin 39 | 40 | shutdown 41 | echo "Done." 42 | -------------------------------------------------------------------------------- /owncast/build/javascript/README.md: -------------------------------------------------------------------------------- 1 | ## Third party web dependencies 2 | 3 | Owncast's web frontend utilizes a few third party Javascript and CSS dependencies that we ship with the application. 4 | 5 | To add, remove, or update one of these components: 6 | 7 | 1. Perform your `npm install/uninstall/etc`, or edit the `package.json` file to reflect the change you want to make. 8 | 2. Edit the `snowpack` `install` block of `package.json` to specify what files you want to add to the Owncast project. This can be an entire library (such as `preact`) or a single file (such as `video.js/dist/video.min.js`). These paths point to files that live in `node_modules`. 9 | 3. Run `npm run build`. This will download the requested module from NPM, package up the assets you specified, and then copy them to the Owncast web app in the `webroot/js/web_modules` directory. 10 | 4. Your new web dependency is now available for use in your web code. 11 | 12 | ## VideoJS versions 13 | 14 | Currently Videojs version 7.8.3 and http-streaming version 2.2.0 are hardcoded because these are versions that have been found to work properly with our HLS stream. Other versions have had issues with things like discontinuities causing a loading spinner. 15 | 16 | So if you update videojs or vhs make sure you do an end-to-end test of a stream and make sure the "this stream is offline" ending video displays properly. 17 | -------------------------------------------------------------------------------- /owncast/build/javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owncast-dependencies", 3 | "version": "1.0.0", 4 | "description": "Javascript dependencies for Owncast web app", 5 | "main": "index.js", 6 | "dependencies": { 7 | "@joeattardi/emoji-button": "^4.6.2", 8 | "@videojs/themes": "^1.0.1", 9 | "htm": "^3.1.0", 10 | "mark.js": "^8.11.1", 11 | "micromodal": "^0.4.10", 12 | "preact": "10.6.6", 13 | "tailwindcss": "^1.9.6", 14 | "video.js": "7.17.0" 15 | }, 16 | "devDependencies": { 17 | "cssnano": "5.1.0", 18 | "postcss": "8.4.7", 19 | "postcss-cli": "9.1.0" 20 | }, 21 | "snowpack": { 22 | "install": [ 23 | "@videojs/themes/fantasy/*", 24 | "video.js/dist/video-js.min.css", 25 | "video.js/dist/video.min.js", 26 | "@joeattardi/emoji-button", 27 | "htm", 28 | "preact", 29 | "preact/hooks", 30 | "mark.js/dist/mark.es6.min.js", 31 | "tailwindcss/dist/tailwind.min.css", 32 | "micromodal/dist/micromodal.min.js" 33 | ] 34 | }, 35 | "scripts": { 36 | "test": "echo \"Error: no test specified\" && exit 1", 37 | "build": "npm install && npx snowpack@2.18.4 install && cp node_modules/video.js/dist/video-js.min.css web_modules/videojs && rm -rf ../../webroot/js/web_modules && cp -R web_modules ../../webroot/js" 38 | }, 39 | "author": "Owncast", 40 | "license": "ISC" 41 | } 42 | -------------------------------------------------------------------------------- /owncast/build/javascript/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('cssnano')({ 4 | preset: 'default', 5 | }), 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /owncast/build/javascript/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: { 3 | enabled: true, 4 | mode: 'layers', 5 | content: ['../../webroot/js/**'], 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /owncast/build/release/docker-nightly.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Docker build 4 | # Must authenticate first: https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages#authenticating-to-github-packages 5 | DOCKER_IMAGE="owncast" 6 | DATE=$(date +"%Y%m%d") 7 | VERSION="${DATE}-nightly" 8 | 9 | echo "Building Docker image ${DOCKER_IMAGE}..." 10 | 11 | # Change to the root directory of the repository 12 | cd $(git rev-parse --show-toplevel) 13 | 14 | earthly --ci --push +docker-all --image="ghcr.io/owncast/${DOCKER_IMAGE}" --tag=nightly --version="${VERSION}" 15 | -------------------------------------------------------------------------------- /owncast/build/release/docker-webv2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Docker build 4 | # Must authenticate first: https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages#authenticating-to-github-packages 5 | DOCKER_IMAGE="owncast" 6 | DATE=$(date +"%Y%m%d") 7 | TAG="webv2" 8 | VERSION="${DATE}-${TAG}" 9 | echo "Building Docker image ${DOCKER_IMAGE}..." 10 | 11 | # Change to the root directory of the repository 12 | cd $(git rev-parse --show-toplevel) 13 | git checkout webv2 14 | 15 | earthly --ci --push +docker-all --image="ghcr.io/owncast/${DOCKER_IMAGE}" --tag=${TAG} --version="${VERSION}" 16 | -------------------------------------------------------------------------------- /owncast/config/configUtils.go: -------------------------------------------------------------------------------- 1 | package config 2 | -------------------------------------------------------------------------------- /owncast/config/constants.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "path/filepath" 4 | 5 | const ( 6 | // StaticVersionNumber is the version of Owncast that is used when it's not overwritten via build-time settings. 7 | StaticVersionNumber = "0.0.13" // Shown when you build from develop 8 | // WebRoot is the web server root directory. 9 | WebRoot = "webroot" 10 | // FfmpegSuggestedVersion is the version of ffmpeg we suggest. 11 | FfmpegSuggestedVersion = "v4.1.5" // Requires the v 12 | // DataDirectory is the directory we save data to. 13 | DataDirectory = "data" 14 | // EmojiDir is relative to the webroot. 15 | EmojiDir = "/img/emoji" 16 | // MaxChatDisplayNameLength is the maximum length of a chat display name. 17 | MaxChatDisplayNameLength = 30 18 | ) 19 | 20 | var ( 21 | // BackupDirectory is the directory we write backup files to. 22 | BackupDirectory = filepath.Join(DataDirectory, "backup") 23 | 24 | // HLSStoragePath is the directory HLS video is written to. 25 | HLSStoragePath = filepath.Join(DataDirectory, "hls") 26 | ) 27 | -------------------------------------------------------------------------------- /owncast/config/updaterConfig_enabled.go: -------------------------------------------------------------------------------- 1 | //go:build enable_updates 2 | // +build enable_updates 3 | 4 | package config 5 | 6 | func init() { 7 | EnableAutoUpdate = true 8 | } 9 | -------------------------------------------------------------------------------- /owncast/controllers/admin.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/owncast/owncast/core/rtmp" 7 | ) 8 | 9 | // DisconnectInboundConnection will force-disconnect an inbound stream. 10 | func DisconnectInboundConnection(w http.ResponseWriter, r *http.Request) { 11 | rtmp.Disconnect() 12 | w.WriteHeader(http.StatusOK) 13 | } 14 | -------------------------------------------------------------------------------- /owncast/controllers/admin/connectedClients.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/owncast/owncast/controllers" 8 | "github.com/owncast/owncast/core/chat" 9 | "github.com/owncast/owncast/core/user" 10 | ) 11 | 12 | // GetConnectedChatClients returns currently connected clients. 13 | func GetConnectedChatClients(w http.ResponseWriter, r *http.Request) { 14 | clients := chat.GetClients() 15 | w.Header().Set("Content-Type", "application/json") 16 | 17 | if err := json.NewEncoder(w).Encode(clients); err != nil { 18 | controllers.InternalErrorHandler(w, err) 19 | } 20 | } 21 | 22 | // ExternalGetConnectedChatClients returns currently connected clients. 23 | func ExternalGetConnectedChatClients(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { 24 | GetConnectedChatClients(w, r) 25 | } 26 | -------------------------------------------------------------------------------- /owncast/controllers/admin/disconnect.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/owncast/owncast/controllers" 7 | "github.com/owncast/owncast/core" 8 | 9 | "github.com/owncast/owncast/core/rtmp" 10 | ) 11 | 12 | // DisconnectInboundConnection will force-disconnect an inbound stream. 13 | func DisconnectInboundConnection(w http.ResponseWriter, r *http.Request) { 14 | if !core.GetStatus().Online { 15 | controllers.WriteSimpleResponse(w, false, "no inbound stream connected") 16 | return 17 | } 18 | 19 | rtmp.Disconnect() 20 | controllers.WriteSimpleResponse(w, true, "inbound stream disconnected") 21 | } 22 | -------------------------------------------------------------------------------- /owncast/controllers/admin/hardware.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/owncast/owncast/metrics" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // GetHardwareStats will return hardware utilization over time. 12 | func GetHardwareStats(w http.ResponseWriter, r *http.Request) { 13 | m := metrics.GetMetrics() 14 | 15 | w.Header().Set("Content-Type", "application/json") 16 | err := json.NewEncoder(w).Encode(m) 17 | if err != nil { 18 | log.Errorln(err) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /owncast/controllers/admin/yp.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/owncast/owncast/controllers" 7 | "github.com/owncast/owncast/core/data" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // ResetYPRegistration will clear the YP protocol registration key. 12 | func ResetYPRegistration(w http.ResponseWriter, r *http.Request) { 13 | log.Traceln("Resetting YP registration key") 14 | if err := data.SetDirectoryRegistrationKey(""); err != nil { 15 | log.Errorln(err) 16 | controllers.WriteSimpleResponse(w, false, err.Error()) 17 | return 18 | } 19 | controllers.WriteSimpleResponse(w, true, "reset") 20 | } 21 | -------------------------------------------------------------------------------- /owncast/controllers/constants.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | // POST is the HTTP POST method. 4 | const POST = "POST" 5 | 6 | // GET is the HTTP GET method. 7 | const GET = "GET" 8 | -------------------------------------------------------------------------------- /owncast/controllers/embed.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/owncast/owncast/router/middleware" 7 | ) 8 | 9 | // GetChatEmbedreadwrite gets the embed for readwrite chat. 10 | func GetChatEmbedreadwrite(w http.ResponseWriter, r *http.Request) { 11 | // Set our global HTTP headers 12 | middleware.SetHeaders(w) 13 | 14 | http.ServeFile(w, r, "webroot/index-standalone-chat-readwrite.html") 15 | } 16 | 17 | // GetChatEmbedreadonly gets the embed for readonly chat. 18 | func GetChatEmbedreadonly(w http.ResponseWriter, r *http.Request) { 19 | // Set our global HTTP headers 20 | middleware.SetHeaders(w) 21 | 22 | http.ServeFile(w, r, "webroot/index-standalone-chat-readonly.html") 23 | } 24 | 25 | // GetVideoEmbed gets the embed for video. 26 | func GetVideoEmbed(w http.ResponseWriter, r *http.Request) { 27 | // Set our global HTTP headers 28 | middleware.SetHeaders(w) 29 | 30 | http.ServeFile(w, r, "webroot/index-video-only.html") 31 | } 32 | -------------------------------------------------------------------------------- /owncast/controllers/followers.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/owncast/owncast/activitypub/persistence" 7 | ) 8 | 9 | // GetFollowers will handle an API request to fetch the list of followers (non-activitypub response). 10 | func GetFollowers(offset int, limit int, w http.ResponseWriter, r *http.Request) { 11 | followers, total, err := persistence.GetFederationFollowers(limit, offset) 12 | if err != nil { 13 | WriteSimpleResponse(w, false, "unable to fetch followers") 14 | return 15 | } 16 | 17 | response := PaginatedResponse{ 18 | Total: total, 19 | Results: followers, 20 | } 21 | WriteResponse(w, response) 22 | } 23 | -------------------------------------------------------------------------------- /owncast/controllers/pagination.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | // PaginatedResponse is a structure for returning a total count with results. 4 | type PaginatedResponse struct { 5 | Total int `json:"total"` 6 | Results interface{} `json:"results"` 7 | } 8 | -------------------------------------------------------------------------------- /owncast/controllers/ping.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/owncast/owncast/core" 7 | "github.com/owncast/owncast/models" 8 | ) 9 | 10 | // Ping is fired by a client to show they are still an active viewer. 11 | func Ping(w http.ResponseWriter, r *http.Request) { 12 | viewer := models.GenerateViewerFromRequest(r) 13 | core.SetViewerActive(&viewer) 14 | w.WriteHeader(http.StatusOK) 15 | } 16 | -------------------------------------------------------------------------------- /owncast/core/chat/concurrentConnections.go: -------------------------------------------------------------------------------- 1 | // nolint:goimports 2 | //go:build !freebsd && !windows 3 | // +build !freebsd,!windows 4 | 5 | package chat 6 | 7 | import ( 8 | "syscall" 9 | 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func setSystemConcurrentConnectionLimit(limit int64) { 14 | var rLimit syscall.Rlimit 15 | if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil { 16 | log.Fatalln(err) 17 | } 18 | 19 | originalLimit := rLimit.Cur 20 | rLimit.Cur = uint64(limit) 21 | if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil { 22 | log.Fatalln(err) 23 | } 24 | 25 | log.Traceln("Max process connection count changed from system limit of", originalLimit, "to", limit) 26 | } 27 | -------------------------------------------------------------------------------- /owncast/core/chat/concurrentConnections_freebsd.go: -------------------------------------------------------------------------------- 1 | // +build freebsd 2 | 3 | package chat 4 | 5 | import ( 6 | "syscall" 7 | 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func setSystemConcurrentConnectionLimit(limit int64) { 12 | var rLimit syscall.Rlimit 13 | if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil { 14 | log.Fatalln(err) 15 | } 16 | 17 | originalLimit := rLimit.Cur 18 | rLimit.Cur = int64(limit) 19 | if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil { 20 | log.Fatalln(err) 21 | } 22 | 23 | log.Traceln("Max process connection count changed from system limit of", originalLimit, "to", limit) 24 | } 25 | -------------------------------------------------------------------------------- /owncast/core/chat/concurrentConnections_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package chat 5 | 6 | func setSystemConcurrentConnectionLimit(limit int64) {} 7 | -------------------------------------------------------------------------------- /owncast/core/chat/events/actionEvent.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | // ActionEvent represents an action that took place, not a chat message. 4 | type ActionEvent struct { 5 | Event 6 | MessageEvent 7 | } 8 | 9 | // GetBroadcastPayload will return the object to send to all chat users. 10 | func (e *ActionEvent) GetBroadcastPayload() EventPayload { 11 | return EventPayload{ 12 | "id": e.ID, 13 | "timestamp": e.Timestamp, 14 | "body": e.Body, 15 | "type": e.GetMessageType(), 16 | } 17 | } 18 | 19 | // GetMessageType will return the type of message. 20 | func (e *ActionEvent) GetMessageType() EventType { 21 | return ChatActionSent 22 | } 23 | -------------------------------------------------------------------------------- /owncast/core/chat/events/connectedClientInfo.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import "github.com/owncast/owncast/core/user" 4 | 5 | // ConnectedClientInfo represents the information about a connected client. 6 | type ConnectedClientInfo struct { 7 | Event 8 | User *user.User `json:"user"` 9 | } 10 | -------------------------------------------------------------------------------- /owncast/core/chat/events/fediverseEngagementEvent.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import "github.com/owncast/owncast/core/data" 4 | 5 | // FediverseEngagementEvent is a message displayed in chat on representing an action on the Fediverse. 6 | type FediverseEngagementEvent struct { 7 | Event 8 | MessageEvent 9 | Image *string `json:"image"` 10 | Link string `json:"link"` 11 | UserAccountName string `json:"title"` 12 | } 13 | 14 | // GetBroadcastPayload will return the object to send to all chat users. 15 | func (e *FediverseEngagementEvent) GetBroadcastPayload() EventPayload { 16 | return EventPayload{ 17 | "id": e.ID, 18 | "timestamp": e.Timestamp, 19 | "body": e.Body, 20 | "image": e.Image, 21 | "type": e.Event.Type, 22 | "title": e.UserAccountName, 23 | "link": e.Link, 24 | "user": EventPayload{ 25 | "displayName": data.GetServerName(), 26 | }, 27 | } 28 | } 29 | 30 | // GetMessageType will return the event type for this message. 31 | func (e *FediverseEngagementEvent) GetMessageType() EventType { 32 | return e.Event.Type 33 | } 34 | -------------------------------------------------------------------------------- /owncast/core/chat/events/nameChangeEvent.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | // NameChangeEvent is received when a user changes their chat display name. 4 | type NameChangeEvent struct { 5 | Event 6 | UserEvent 7 | NewName string `json:"newName"` 8 | } 9 | 10 | // NameChangeBroadcast represents a user changing their chat display name. 11 | type NameChangeBroadcast struct { 12 | Event 13 | OutboundEvent 14 | UserEvent 15 | Oldname string `json:"oldName"` 16 | } 17 | 18 | // GetBroadcastPayload will return the object to send to all chat users. 19 | func (e *NameChangeBroadcast) GetBroadcastPayload() EventPayload { 20 | return EventPayload{ 21 | "id": e.ID, 22 | "timestamp": e.Timestamp, 23 | "user": e.User, 24 | "oldName": e.Oldname, 25 | "type": UserNameChanged, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /owncast/core/chat/events/setMessageVisibilityEvent.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | // SetMessageVisibilityEvent is the event fired when one or more message 4 | // visibilities are changed. 5 | type SetMessageVisibilityEvent struct { 6 | Event 7 | UserMessageEvent 8 | MessageIDs []string 9 | Visible bool 10 | } 11 | 12 | // GetBroadcastPayload will return the object to send to all chat users. 13 | func (e *SetMessageVisibilityEvent) GetBroadcastPayload() EventPayload { 14 | return EventPayload{ 15 | "type": VisibiltyUpdate, 16 | "id": e.ID, 17 | "timestamp": e.Timestamp, 18 | "ids": e.MessageIDs, 19 | "visible": e.Visible, 20 | } 21 | } 22 | 23 | // GetMessageType will return the event type for this message. 24 | func (e *SetMessageVisibilityEvent) GetMessageType() EventType { 25 | return VisibiltyUpdate 26 | } 27 | -------------------------------------------------------------------------------- /owncast/core/chat/events/systemMessageEvent.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import "github.com/owncast/owncast/core/data" 4 | 5 | // SystemMessageEvent is a message displayed in chat on behalf of the server. 6 | type SystemMessageEvent struct { 7 | Event 8 | MessageEvent 9 | } 10 | 11 | // GetBroadcastPayload will return the object to send to all chat users. 12 | func (e *SystemMessageEvent) GetBroadcastPayload() EventPayload { 13 | return EventPayload{ 14 | "id": e.ID, 15 | "timestamp": e.Timestamp, 16 | "body": e.Body, 17 | "type": SystemMessageSent, 18 | "user": EventPayload{ 19 | "displayName": data.GetServerName(), 20 | }, 21 | } 22 | } 23 | 24 | // GetMessageType will return the event type for this message. 25 | func (e *SystemMessageEvent) GetMessageType() EventType { 26 | return SystemMessageSent 27 | } 28 | -------------------------------------------------------------------------------- /owncast/core/chat/events/userDisabledEvent.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | // UserDisabledEvent is the event fired when a user is banned/blocked and disconnected from chat. 4 | type UserDisabledEvent struct { 5 | Event 6 | UserEvent 7 | } 8 | 9 | // GetBroadcastPayload will return the object to send to all chat users. 10 | func (e *UserDisabledEvent) GetBroadcastPayload() EventPayload { 11 | return EventPayload{ 12 | "type": ErrorUserDisabled, 13 | "id": e.ID, 14 | "timestamp": e.Timestamp, 15 | "user": e.User, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /owncast/core/chat/events/userJoinedEvent.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | // UserJoinedEvent is the event fired when a user joins chat. 4 | type UserJoinedEvent struct { 5 | Event 6 | UserEvent 7 | } 8 | 9 | // GetBroadcastPayload will return the object to send to all chat users. 10 | func (e *UserJoinedEvent) GetBroadcastPayload() EventPayload { 11 | return EventPayload{ 12 | "type": UserJoined, 13 | "id": e.ID, 14 | "timestamp": e.Timestamp, 15 | "user": e.User, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /owncast/core/chat/events/userMessageEvent.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | // UserMessageEvent is an inbound message from a user. 4 | type UserMessageEvent struct { 5 | Event 6 | UserEvent 7 | MessageEvent 8 | } 9 | 10 | // GetBroadcastPayload will return the object to send to all chat users. 11 | func (e *UserMessageEvent) GetBroadcastPayload() EventPayload { 12 | return EventPayload{ 13 | "id": e.ID, 14 | "timestamp": e.Timestamp, 15 | "body": e.Body, 16 | "user": e.User, 17 | "type": MessageSent, 18 | "visible": e.HiddenAt == nil, 19 | } 20 | } 21 | 22 | // GetMessageType will return the event type for this message. 23 | func (e *UserMessageEvent) GetMessageType() EventType { 24 | return MessageSent 25 | } 26 | -------------------------------------------------------------------------------- /owncast/core/chat/messages.go: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/owncast/owncast/core/chat/events" 7 | "github.com/owncast/owncast/core/webhooks" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // SetMessagesVisibility will set the visibility of multiple messages by ID. 12 | func SetMessagesVisibility(messageIDs []string, visibility bool) error { 13 | // Save new message visibility 14 | if err := saveMessageVisibility(messageIDs, visibility); err != nil { 15 | log.Errorln(err) 16 | return err 17 | } 18 | 19 | // Send an event letting the chat clients know to hide or show 20 | // the messages. 21 | event := events.SetMessageVisibilityEvent{ 22 | MessageIDs: messageIDs, 23 | Visible: visibility, 24 | } 25 | event.Event.SetDefaults() 26 | 27 | payload := event.GetBroadcastPayload() 28 | if err := _server.Broadcast(payload); err != nil { 29 | return errors.New("error broadcasting message visibility payload " + err.Error()) 30 | } 31 | 32 | webhooks.SendChatEventSetMessageVisibility(event) 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /owncast/core/chat/pruner.go: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import ( 4 | "fmt" 5 | 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // Only keep recent messages so we don't keep more chat data than needed 10 | // for privacy and efficiency reasons. 11 | func runPruner() { 12 | _datastore.DbLock.Lock() 13 | defer _datastore.DbLock.Unlock() 14 | 15 | log.Traceln("Removing chat messages older than", maxBacklogHours, "hours") 16 | 17 | deleteStatement := `DELETE FROM messages WHERE timestamp <= datetime('now', 'localtime', ?)` 18 | tx, err := _datastore.DB.Begin() 19 | if err != nil { 20 | log.Debugln(err) 21 | return 22 | } 23 | 24 | stmt, err := tx.Prepare(deleteStatement) 25 | if err != nil { 26 | log.Debugln(err) 27 | return 28 | } 29 | defer stmt.Close() 30 | 31 | if _, err = stmt.Exec(fmt.Sprintf("-%d hours", maxBacklogHours)); err != nil { 32 | log.Debugln(err) 33 | return 34 | } 35 | if err = tx.Commit(); err != nil { 36 | log.Debugln(err) 37 | return 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /owncast/core/chat/utils.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package chat 5 | 6 | import ( 7 | "syscall" 8 | 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func getMaximumConcurrentConnectionLimit() int64 { 13 | var rLimit syscall.Rlimit 14 | if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil { 15 | log.Fatalln(err) 16 | } 17 | 18 | // Return the limit to 70% of max so the machine doesn't die even if it's maxed out for some reason. 19 | proposedLimit := int64(float32(rLimit.Max) * 0.7) 20 | 21 | return proposedLimit 22 | } 23 | -------------------------------------------------------------------------------- /owncast/core/chat/utils_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package chat 5 | 6 | func getMaximumConcurrentConnectionLimit() int64 { 7 | // The maximum limit I can find for windows is 16,777,216 8 | // (essentially unlimited, but add the 0.7 multiplier as well to be 9 | // consistent with other systems) 10 | // https://docs.microsoft.com/en-gb/archive/blogs/markrussinovich/pushing-the-limits-of-windows-handles 11 | return (16777216 * 7) / 10 12 | } 13 | -------------------------------------------------------------------------------- /owncast/core/data/activitypub.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | // GetFederatedInboxMap is a mapping between account names and their outbox. 4 | func GetFederatedInboxMap() map[string]string { 5 | return map[string]string{ 6 | GetDefaultFederationUsername(): GetDefaultFederationUsername(), 7 | } 8 | } 9 | 10 | // GetDefaultFederationUsername will return the username used for sending federation activities. 11 | func GetDefaultFederationUsername() string { 12 | return GetFederationUsername() 13 | } 14 | -------------------------------------------------------------------------------- /owncast/core/data/cache.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | ) 7 | 8 | var _cacheLock = sync.Mutex{} 9 | 10 | // GetCachedValue will return a value for key from the cache. 11 | func (ds *Datastore) GetCachedValue(key string) ([]byte, error) { 12 | _cacheLock.Lock() 13 | defer _cacheLock.Unlock() 14 | 15 | // Check for a cached value 16 | if val, ok := ds.cache[key]; ok { 17 | return val, nil 18 | } 19 | 20 | return nil, errors.New(key + " not found in cache") 21 | } 22 | 23 | // SetCachedValue will set a value for key in the cache. 24 | func (ds *Datastore) SetCachedValue(key string, b []byte) { 25 | _cacheLock.Lock() 26 | defer _cacheLock.Unlock() 27 | 28 | ds.cache[key] = b 29 | } 30 | -------------------------------------------------------------------------------- /owncast/core/data/configEntry.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | ) 7 | 8 | // ConfigEntry is the actual object saved to the database. 9 | // The Value is encoded using encoding/gob. 10 | type ConfigEntry struct { 11 | Key string 12 | Value interface{} 13 | } 14 | 15 | func (c *ConfigEntry) getStringSlice() ([]string, error) { 16 | decoder := c.getDecoder() 17 | var result []string 18 | err := decoder.Decode(&result) 19 | return result, err 20 | } 21 | 22 | func (c *ConfigEntry) getString() (string, error) { 23 | decoder := c.getDecoder() 24 | var result string 25 | err := decoder.Decode(&result) 26 | return result, err 27 | } 28 | 29 | func (c *ConfigEntry) getNumber() (float64, error) { 30 | decoder := c.getDecoder() 31 | var result float64 32 | err := decoder.Decode(&result) 33 | return result, err 34 | } 35 | 36 | func (c *ConfigEntry) getBool() (bool, error) { 37 | decoder := c.getDecoder() 38 | var result bool 39 | err := decoder.Decode(&result) 40 | return result, err 41 | } 42 | 43 | func (c *ConfigEntry) getObject(result interface{}) error { 44 | decoder := c.getDecoder() 45 | err := decoder.Decode(result) 46 | return err 47 | } 48 | 49 | func (c *ConfigEntry) getDecoder() *gob.Decoder { 50 | valueBytes := c.Value.([]byte) 51 | decoder := gob.NewDecoder(bytes.NewBuffer(valueBytes)) 52 | return decoder 53 | } 54 | -------------------------------------------------------------------------------- /owncast/core/data/crypto.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | // GetPublicKey will return the public key. 4 | func GetPublicKey() string { 5 | value, _ := _datastore.GetString(publicKeyKey) 6 | return value 7 | } 8 | 9 | // SetPublicKey will save the public key. 10 | func SetPublicKey(key string) error { 11 | return _datastore.SetString(publicKeyKey, key) 12 | } 13 | 14 | // GetPrivateKey will return the private key. 15 | func GetPrivateKey() string { 16 | value, _ := _datastore.GetString(privateKeyKey) 17 | return value 18 | } 19 | 20 | // SetPrivateKey will save the private key. 21 | func SetPrivateKey(key string) error { 22 | return _datastore.SetString(privateKeyKey, key) 23 | } 24 | -------------------------------------------------------------------------------- /owncast/core/data/utils.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "database/sql" 5 | 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // MustExec will execute a SQL statement on a provided database instance. 10 | func MustExec(s string, db *sql.DB) { 11 | stmt, err := db.Prepare(s) 12 | if err != nil { 13 | log.Panic(err) 14 | } 15 | defer stmt.Close() 16 | _, err = stmt.Exec() 17 | if err != nil { 18 | log.Warnln(err) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /owncast/core/playlist/writer.go: -------------------------------------------------------------------------------- 1 | package playlist 2 | 3 | import "os" 4 | 5 | // WritePlaylist writes the playlist to disk. 6 | func WritePlaylist(data string, filePath string) error { 7 | // nolint:gosec 8 | f, err := os.Create(filePath) 9 | if err != nil { 10 | return err 11 | } 12 | defer f.Close() 13 | 14 | if _, err := f.WriteString(data); err != nil { 15 | return err 16 | } 17 | 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /owncast/core/rtmp/broadcaster.go: -------------------------------------------------------------------------------- 1 | package rtmp 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/nareix/joy5/format/flv/flvio" 7 | "github.com/owncast/owncast/models" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func setCurrentBroadcasterInfo(t flvio.Tag, remoteAddr string) { 12 | data, err := getInboundDetailsFromMetadata(t.DebugFields()) 13 | if err != nil { 14 | log.Traceln("Unable to parse inbound broadcaster details:", err) 15 | } 16 | 17 | broadcaster := models.Broadcaster{ 18 | RemoteAddr: remoteAddr, 19 | Time: time.Now(), 20 | StreamDetails: models.InboundStreamDetails{ 21 | Width: data.Width, 22 | Height: data.Height, 23 | VideoBitrate: int(data.VideoBitrate), 24 | VideoCodec: getVideoCodec(data.VideoCodec), 25 | VideoFramerate: data.VideoFramerate, 26 | AudioBitrate: int(data.AudioBitrate), 27 | AudioCodec: getAudioCodec(data.AudioCodec), 28 | Encoder: data.Encoder, 29 | VideoOnly: data.AudioCodec == nil, 30 | }, 31 | } 32 | 33 | _setBroadcaster(broadcaster) 34 | } 35 | -------------------------------------------------------------------------------- /owncast/core/rtmp/utils_test.go: -------------------------------------------------------------------------------- 1 | package rtmp 2 | 3 | import "testing" 4 | 5 | func Test_secretMatch(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | streamKey string 9 | path string 10 | want bool 11 | }{ 12 | {"positive", "abc", "/live/abc", true}, 13 | {"negative", "abc", "/live/def", false}, 14 | {"positive with numbers", "abc123", "/live/abc123", true}, 15 | {"negative with numbers", "abc123", "/live/def456", false}, 16 | {"positive with url chars", "one/two/three", "/live/one/two/three", true}, 17 | {"negative with url chars", "one/two/three", "/live/four/five/six", false}, 18 | {"check the entire secret", "three", "/live/one/two/three", false}, 19 | {"with /live/ in secret", "one/live/three", "/live/one/live/three", true}, 20 | {"bad path", "anything", "nonsense", false}, 21 | {"missing secret", "abc", "/live/", false}, 22 | {"missing secret and missing last slash", "abc", "/live", false}, 23 | {"streamkey before /live/", "streamkey", "/streamkey/live", false}, 24 | {"missing /live/", "anything", "/something/else", false}, 25 | {"stuff before and after /live/", "after", "/before/live/after", false}, 26 | } 27 | 28 | for _, tt := range tests { 29 | t.Run(tt.name, func(t *testing.T) { 30 | if got := secretMatch(tt.streamKey, tt.path); got != tt.want { 31 | t.Errorf("secretMatch() = %v, want %v", got, tt.want) 32 | } 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /owncast/core/status.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/owncast/owncast/config" 5 | "github.com/owncast/owncast/core/data" 6 | "github.com/owncast/owncast/models" 7 | ) 8 | 9 | // GetStatus gets the status of the system. 10 | func GetStatus() models.Status { 11 | if _stats == nil { 12 | return models.Status{} 13 | } 14 | 15 | viewerCount := 0 16 | if IsStreamConnected() { 17 | viewerCount = len(_stats.Viewers) 18 | } 19 | 20 | return models.Status{ 21 | Online: IsStreamConnected(), 22 | ViewerCount: viewerCount, 23 | OverallMaxViewerCount: _stats.OverallMaxViewerCount, 24 | SessionMaxViewerCount: _stats.SessionMaxViewerCount, 25 | LastDisconnectTime: _stats.LastDisconnectTime, 26 | LastConnectTime: _stats.LastConnectTime, 27 | VersionNumber: config.VersionNumber, 28 | StreamTitle: data.GetStreamTitle(), 29 | } 30 | } 31 | 32 | // GetCurrentBroadcast will return the currently active broadcast. 33 | func GetCurrentBroadcast() *models.CurrentBroadcast { 34 | return _currentBroadcast 35 | } 36 | 37 | // setBroadcaster will store the current inbound broadcasting details. 38 | func setBroadcaster(broadcaster models.Broadcaster) { 39 | _broadcaster = &broadcaster 40 | } 41 | 42 | // GetBroadcaster will return the details of the currently active broadcaster. 43 | func GetBroadcaster() *models.Broadcaster { 44 | return _broadcaster 45 | } 46 | -------------------------------------------------------------------------------- /owncast/core/storage.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/owncast/owncast/core/data" 5 | "github.com/owncast/owncast/core/storageproviders" 6 | ) 7 | 8 | func setupStorage() error { 9 | s3Config := data.GetS3Config() 10 | 11 | if s3Config.Enabled { 12 | _storage = storageproviders.NewS3Storage() 13 | } else { 14 | _storage = storageproviders.NewLocalStorage() 15 | } 16 | 17 | if err := _storage.Setup(); err != nil { 18 | return err 19 | } 20 | 21 | handler.Storage = _storage 22 | 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /owncast/core/transcoder/hlsHandler.go: -------------------------------------------------------------------------------- 1 | package transcoder 2 | 3 | import ( 4 | "github.com/owncast/owncast/models" 5 | ) 6 | 7 | // HLSHandler gets told about available HLS playlists and segments. 8 | type HLSHandler struct { 9 | Storage models.StorageProvider 10 | } 11 | 12 | // SegmentWritten is fired when a HLS segment is written to disk. 13 | func (h *HLSHandler) SegmentWritten(localFilePath string) { 14 | h.Storage.SegmentWritten(localFilePath) 15 | } 16 | 17 | // VariantPlaylistWritten is fired when a HLS variant playlist is written to disk. 18 | func (h *HLSHandler) VariantPlaylistWritten(localFilePath string) { 19 | h.Storage.VariantPlaylistWritten(localFilePath) 20 | } 21 | 22 | // MasterPlaylistWritten is fired when a HLS master playlist is written to disk. 23 | func (h *HLSHandler) MasterPlaylistWritten(localFilePath string) { 24 | h.Storage.MasterPlaylistWritten(localFilePath) 25 | } 26 | -------------------------------------------------------------------------------- /owncast/core/webhooks/stream.go: -------------------------------------------------------------------------------- 1 | package webhooks 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/owncast/owncast/core/data" 7 | "github.com/owncast/owncast/models" 8 | "github.com/teris-io/shortid" 9 | ) 10 | 11 | // SendStreamStatusEvent will send all webhook destinations the current stream status. 12 | func SendStreamStatusEvent(eventType models.EventType) { 13 | sendStreamStatusEvent(eventType, shortid.MustGenerate(), time.Now()) 14 | } 15 | 16 | func sendStreamStatusEvent(eventType models.EventType, id string, timestamp time.Time) { 17 | SendEventToWebhooks(WebhookEvent{ 18 | Type: eventType, 19 | EventData: map[string]interface{}{ 20 | "id": id, 21 | "name": data.GetServerName(), 22 | "summary": data.GetServerSummary(), 23 | "streamTitle": data.GetStreamTitle(), 24 | "timestamp": timestamp, 25 | }, 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /owncast/core/webhooks/stream_test.go: -------------------------------------------------------------------------------- 1 | package webhooks 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/owncast/owncast/core/chat/events" 8 | "github.com/owncast/owncast/core/data" 9 | "github.com/owncast/owncast/models" 10 | ) 11 | 12 | func TestSendStreamStatusEvent(t *testing.T) { 13 | data.SetServerName("my server") 14 | data.SetServerSummary("my server where I stream") 15 | data.SetStreamTitle("my stream") 16 | 17 | checkPayload(t, models.StreamStarted, func() { 18 | sendStreamStatusEvent(events.StreamStarted, "id", time.Unix(72, 6).UTC()) 19 | }, `{ 20 | "id": "id", 21 | "name": "my server", 22 | "streamTitle": "my stream", 23 | "summary": "my server where I stream", 24 | "timestamp": "1970-01-01T00:01:12.000000006Z" 25 | }`) 26 | } 27 | -------------------------------------------------------------------------------- /owncast/db/README.md: -------------------------------------------------------------------------------- 1 | # SQL Queries 2 | 3 | sqlc generates **type-safe code** from SQL. Here's how it works: 4 | 5 | 1. You define the schema in `schema.sql`. 6 | 1. You write your queries in `query.sql` using regular SQL. 7 | 1. You run `sqlc generate` to generate Go code with type-safe interfaces to those queries. 8 | 1. You write application code that calls the generated code. 9 | 10 | Only those who need to create or update SQL queries will need to have `sqlc` installed on their system. **It is not a dependency required to build the codebase.** 11 | 12 | ## Install sqlc 13 | 14 | ### Snap 15 | 16 | `sudo snap install sqlc` 17 | 18 | ### Go install 19 | 20 | `go install github.com/kyleconroy/sqlc/cmd/sqlc@latest` 21 | 22 | ### macOS 23 | 24 | `brew install sqlc` 25 | 26 | ### Download a release 27 | 28 | Visit to download a release for your environment. 29 | -------------------------------------------------------------------------------- /owncast/db/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.14.0 4 | 5 | package db 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | ) 11 | 12 | type DBTX interface { 13 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 14 | PrepareContext(context.Context, string) (*sql.Stmt, error) 15 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 16 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 17 | } 18 | 19 | func New(db DBTX) *Queries { 20 | return &Queries{db: db} 21 | } 22 | 23 | type Queries struct { 24 | db DBTX 25 | } 26 | 27 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 28 | return &Queries{ 29 | db: tx, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /owncast/docs/DESIGN.md: -------------------------------------------------------------------------------- 1 | See the design contributions document at 2 | 3 | https://github.com/owncast/owncast/blob/develop/.design/DESIGN.md 4 | -------------------------------------------------------------------------------- /owncast/docs/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Owncast appreciates efforts to improve the security of the software 4 | and follow the [GitHub coordinated disclosure of security vulnerabilities](https://docs.github.com/en/code-security/security-advisories/about-coordinated-disclosure-of-security-vulnerabilities#about-reporting-and-disclosing-vulnerabilities-in-projects-on-github) 5 | for responsible disclosure and prompt mitigation. 6 | 7 | ## Supported Versions 8 | 9 | The latest version of Owncast is seen as the supported version. As a small project we are unable to support previous versions and urge users of the software to stay up to date. 10 | 11 | ## Reporting a Vulnerability 12 | 13 | To report a security issue with Owncast, [open an issue](https://github.com/owncast/owncast/issues/new 14 | ) on the Owncast GitHub repository and *do not* mention vulnerability details in the issue. If you have a preferred next step on where to discuss the details of the disclosure, please mention that in the issue if it's appropriate for those details to be public. 15 | 16 | You may optionally [email Gabe](mailto:gabek@real-ity.com) to alert him directly and provide specifics on how you wish to disclose the details of the issue. 17 | 18 | Owncast may open a draft [GitHub Security Advisory](https://docs.github.com/en/code-security/security-advisories/creating-a-security-advisory) 19 | to discuss the vulnerability details in private if it is warranted. 20 | -------------------------------------------------------------------------------- /owncast/examples/owncast-sample.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Owncast Service 3 | 4 | [Service] 5 | Type=simple 6 | WorkingDirectory=[path to owncast directory] 7 | ReadWritePaths=[path to owncast directory] 8 | ExecStart=[path to owncast directory]/owncast 9 | Restart=always 10 | RestartSec=5 11 | User=[user to run owncast as] 12 | Group=[group to run owncast as] 13 | NoNewPrivileges=true 14 | SecureBits=noroot 15 | ProtectSystem=strict 16 | ProtectHome=read-only 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | -------------------------------------------------------------------------------- /owncast/examples/owncast-systemd-service.md: -------------------------------------------------------------------------------- 1 | This can be any text that makes sense to you. 2 | ``` 3 | [Unit] 4 | Description=Owncast Service 5 | ``` 6 | 7 | This is where the "functional" parts of the service live.
8 | ``` 9 | [Service] 10 | Type=simple 11 | WorkingDirectory=[path_to_owncast_root_directory] 12 | ExecStart=[path_to_owncast_executable] 13 | Restart=on-failure 14 | RestartSec=5 15 | ``` 16 | `WorkingDirectory` should be where you want the owncast folder to live.
17 | 18 | **Example:**
19 | ```WorkingDirectory=/var/www/owncast``` 20 | 21 | Similarly the `ExecStart` is the actual owncast binary.
22 | 23 | **Example:**
24 | ```ExecStart=/var/www/owncast/owncast``` 25 | 26 | ``` 27 | [Install] 28 | WantedBy=multi-user.target 29 | ``` 30 | This just means, use runlevel 3 non-graphical. 31 | 32 | 33 | **INSTALLATION** 34 | Just create the file in your systemd configuraiton directory (typically /etc/systemd/system/), and update the systemd daemon with: 35 | ```$sudo systemd daemon-reload``` 36 | 37 | **USAGE** 38 | Currently the following options work 39 | - Start 40 | - Stop 41 | - Status 42 | -------------------------------------------------------------------------------- /owncast/logging/paths.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/owncast/owncast/config" 7 | ) 8 | 9 | // GetTranscoderLogFilePath returns the logging path for the transcoder log output. 10 | func GetTranscoderLogFilePath() string { 11 | return filepath.Join(config.LogDirectory, "transcoder.log") 12 | } 13 | 14 | func getLogFilePath() string { 15 | return filepath.Join(config.LogDirectory, "owncast.log") 16 | } 17 | -------------------------------------------------------------------------------- /owncast/metrics/timestampedValue.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/nakabonne/tstorage" 7 | ) 8 | 9 | // TimestampedValue is a value with a timestamp. 10 | type TimestampedValue struct { 11 | Time time.Time `json:"time"` 12 | Value float64 `json:"value"` 13 | } 14 | 15 | func makeTimestampedValuesFromDatapoints(dp []*tstorage.DataPoint) []TimestampedValue { 16 | tv := []TimestampedValue{} 17 | for _, d := range dp { 18 | tv = append(tv, TimestampedValue{Time: time.Unix(d.Timestamp, 0), Value: d.Value}) 19 | } 20 | 21 | return tv 22 | } 23 | -------------------------------------------------------------------------------- /owncast/models/baseAPIResponse.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // BaseAPIResponse is a simple response to API requests. 4 | type BaseAPIResponse struct { 5 | Success bool `json:"success"` 6 | Message string `json:"message"` 7 | } 8 | -------------------------------------------------------------------------------- /owncast/models/broadcaster.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | // Broadcaster represents the details around the inbound broadcasting connection. 6 | type Broadcaster struct { 7 | RemoteAddr string `json:"remoteAddr"` 8 | StreamDetails InboundStreamDetails `json:"streamDetails"` 9 | Time time.Time `json:"time"` 10 | } 11 | 12 | // InboundStreamDetails represents an inbound broadcast stream. 13 | type InboundStreamDetails struct { 14 | Width int `json:"width"` 15 | Height int `json:"height"` 16 | VideoFramerate float32 `json:"framerate"` 17 | VideoBitrate int `json:"videoBitrate"` 18 | VideoCodec string `json:"videoCodec"` 19 | AudioBitrate int `json:"audioBitrate"` 20 | AudioCodec string `json:"audioCodec"` 21 | Encoder string `json:"encoder"` 22 | VideoOnly bool `json:"-"` 23 | } 24 | 25 | // RTMPStreamMetadata is the raw metadata that comes in with a RTMP connection. 26 | type RTMPStreamMetadata struct { 27 | Width int `json:"width"` 28 | Height int `json:"height"` 29 | VideoBitrate float32 `json:"videodatarate"` 30 | VideoCodec interface{} `json:"videocodecid"` 31 | VideoFramerate float32 `json:"framerate"` 32 | AudioBitrate float32 `json:"audiodatarate"` 33 | AudioCodec interface{} `json:"audiocodecid"` 34 | Encoder string `json:"encoder"` 35 | } 36 | -------------------------------------------------------------------------------- /owncast/models/client.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/owncast/owncast/geoip" 8 | "github.com/owncast/owncast/utils" 9 | ) 10 | 11 | // ConnectedClientsResponse is the response of the currently connected chat clients. 12 | type ConnectedClientsResponse struct { 13 | Clients []Client `json:"clients"` 14 | } 15 | 16 | // Client represents a single chat client. 17 | type Client struct { 18 | ConnectedAt time.Time `json:"connectedAt"` 19 | LastSeen time.Time `json:"-"` 20 | MessageCount int `json:"messageCount"` 21 | UserAgent string `json:"userAgent"` 22 | IPAddress string `json:"ipAddress"` 23 | Username *string `json:"username"` 24 | ClientID string `json:"clientID"` 25 | Geo *geoip.GeoDetails `json:"geo"` 26 | } 27 | 28 | // GenerateClientFromRequest will return a chat client from a http request. 29 | func GenerateClientFromRequest(req *http.Request) Client { 30 | return Client{ 31 | ConnectedAt: time.Now(), 32 | LastSeen: time.Now(), 33 | MessageCount: 0, 34 | UserAgent: req.UserAgent(), 35 | IPAddress: utils.GetIPAddressFromRequest(req), 36 | Username: nil, 37 | ClientID: utils.GenerateClientIDFromRequest(req), 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /owncast/models/currentBroadcast.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // CurrentBroadcast represents the configuration associated with the currently active stream. 4 | type CurrentBroadcast struct { 5 | OutputSettings []StreamOutputVariant `json:"outputSettings"` 6 | LatencyLevel LatencyLevel `json:"latencyLevel"` 7 | } 8 | -------------------------------------------------------------------------------- /owncast/models/emoji.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // CustomEmoji represents an image that can be used in chat as a custom emoji. 4 | type CustomEmoji struct { 5 | Name string `json:"name"` 6 | Emoji string `json:"emoji"` 7 | } 8 | -------------------------------------------------------------------------------- /owncast/models/eventType.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // EventType is the type of a websocket event. 4 | type EventType = string 5 | 6 | const ( 7 | // MessageSent is the event sent when a chat event takes place. 8 | MessageSent EventType = "CHAT" 9 | // UserJoined is the event sent when a chat user join action takes place. 10 | UserJoined EventType = "USER_JOINED" 11 | // UserNameChanged is the event sent when a chat username change takes place. 12 | UserNameChanged EventType = "NAME_CHANGE" 13 | // VisibiltyToggled is the event sent when a chat message's visibility changes. 14 | VisibiltyToggled EventType = "VISIBILITY-UPDATE" 15 | // PING is a ping message. 16 | PING EventType = "PING" 17 | // PONG is a pong message. 18 | PONG EventType = "PONG" 19 | // StreamStarted represents a stream started event. 20 | StreamStarted EventType = "STREAM_STARTED" 21 | // StreamStopped represents a stream stopped event. 22 | StreamStopped EventType = "STREAM_STOPPED" 23 | // SystemMessageSent is the event sent when a system message is sent. 24 | SystemMessageSent EventType = "SYSTEM" 25 | // ChatActionSent is a generic chat action that can be used for anything that doesn't need specific handling or formatting. 26 | ChatActionSent EventType = "CHAT_ACTION" 27 | ) 28 | -------------------------------------------------------------------------------- /owncast/models/externalAction.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // ExternalAction is a link that will open as a 3rd party action. 4 | type ExternalAction struct { 5 | // URL is the URL to load. 6 | URL string `json:"url"` 7 | // Title is the name of this action, displayed in the modal. 8 | Title string `json:"title"` 9 | // Description is the description of this action. 10 | Description string `json:"description"` 11 | // Icon is the optional icon for the button associated with this action. 12 | Icon string `json:"icon"` 13 | // Color is the optional color for the button associated with this action. 14 | Color string `json:"color"` 15 | // OpenExternally states if the action should open a new tab/window instead of an internal modal. 16 | OpenExternally bool `json:"openExternally"` 17 | } 18 | -------------------------------------------------------------------------------- /owncast/models/federatedActivity.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | // FederatedActivity is an internal representation of an activity that was 6 | // accepted and stored. 7 | type FederatedActivity struct { 8 | IRI string `json:"iri"` 9 | ActorIRI string `json:"actorIRI"` 10 | Type string `json:"type"` 11 | Timestamp time.Time `json:"timestamp"` 12 | } 13 | -------------------------------------------------------------------------------- /owncast/models/follower.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/owncast/owncast/utils" 4 | 5 | // Follower is our internal representation of a single follower within Owncast. 6 | type Follower struct { 7 | // ActorIRI is the IRI of the remote actor. 8 | ActorIRI string `json:"link"` 9 | // Inbox is the inbox URL of the remote follower 10 | Inbox string `json:"-"` 11 | // Name is the display name of the follower. 12 | Name string `json:"name"` 13 | // Username is the account username of the remote actor. 14 | Username string `json:"username"` 15 | // Image is the avatar image of the follower. 16 | Image string `json:"image"` 17 | // Timestamp is when this follow request was created. 18 | Timestamp utils.NullTime `json:"timestamp,omitempty"` 19 | // DisabledAt is when this follower was rejected or disabled. 20 | DisabledAt utils.NullTime `json:"disabledAt,omitempty"` 21 | } 22 | -------------------------------------------------------------------------------- /owncast/models/ipAddress.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | // IPAddress is a simple representation of an IP address. 6 | type IPAddress struct { 7 | IPAddress string `json:"ipAddress"` 8 | Notes string `json:"notes"` 9 | CreatedAt time.Time `json:"createdAt"` 10 | } 11 | -------------------------------------------------------------------------------- /owncast/models/latencyLevels.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // LatencyLevel is a representation of HLS configuration values. 4 | type LatencyLevel struct { 5 | Level int `json:"level"` 6 | SecondsPerSegment int `json:"-"` 7 | SegmentCount int `json:"-"` 8 | } 9 | 10 | // GetLatencyConfigs will return the available latency level options. 11 | func GetLatencyConfigs() map[int]LatencyLevel { 12 | return map[int]LatencyLevel{ 13 | 0: {Level: 0, SecondsPerSegment: 1, SegmentCount: 25}, // Approx 5 seconds 14 | 1: {Level: 1, SecondsPerSegment: 2, SegmentCount: 15}, // Approx 8-9 seconds 15 | 2: {Level: 2, SecondsPerSegment: 3, SegmentCount: 10}, // Default Approx 10 seconds 16 | 3: {Level: 3, SecondsPerSegment: 4, SegmentCount: 8}, // Approx 15 seconds 17 | 4: {Level: 4, SecondsPerSegment: 5, SegmentCount: 5}, // Approx 18 seconds 18 | } 19 | } 20 | 21 | // GetLatencyLevel will return the latency level at index. 22 | func GetLatencyLevel(index int) LatencyLevel { 23 | return GetLatencyConfigs()[index] 24 | } 25 | -------------------------------------------------------------------------------- /owncast/models/notification.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // DiscordConfiguration represents the configuration for the discord 4 | // notification service. 5 | type DiscordConfiguration struct { 6 | Enabled bool `json:"enabled"` 7 | Webhook string `json:"webhook,omitempty"` 8 | GoLiveMessage string `json:"goLiveMessage,omitempty"` 9 | } 10 | 11 | // BrowserNotificationConfiguration represents the configuration for 12 | // browser notifications. 13 | type BrowserNotificationConfiguration struct { 14 | Enabled bool `json:"enabled"` 15 | GoLiveMessage string `json:"goLiveMessage,omitempty"` 16 | } 17 | 18 | // TwitterConfiguration represents the configuration for Twitter access. 19 | type TwitterConfiguration struct { 20 | Enabled bool `json:"enabled"` 21 | APIKey string `json:"apiKey"` // aka consumer key 22 | APISecret string `json:"apiSecret"` // aka consumer secret 23 | AccessToken string `json:"accessToken"` 24 | AccessTokenSecret string `json:"accessTokenSecret"` 25 | BearerToken string `json:"bearerToken"` 26 | GoLiveMessage string `json:"goLiveMessage,omitempty"` 27 | } 28 | -------------------------------------------------------------------------------- /owncast/models/pingMessage.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // PingMessage represents a ping message between the client and server. 4 | type PingMessage struct { 5 | MessageType EventType `json:"type"` 6 | } 7 | -------------------------------------------------------------------------------- /owncast/models/playlist.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Segment represents a segment of the live stream. 4 | type Segment struct { 5 | VariantIndex int // The bitrate variant 6 | FullDiskPath string // Where it lives on disk 7 | RelativeUploadPath string // Path it should have remotely 8 | RemoteURL string 9 | } 10 | 11 | // Variant represents a single video variant and the segments that make it up. 12 | type Variant struct { 13 | VariantIndex int 14 | Segments map[string]*Segment 15 | } 16 | 17 | // GetSegmentForFilename gets the segment for the provided filename. 18 | func (v *Variant) GetSegmentForFilename(filename string) *Segment { 19 | return v.Segments[filename] 20 | } 21 | -------------------------------------------------------------------------------- /owncast/models/s3Storage.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // S3 is the storage configuration. 4 | type S3 struct { 5 | Enabled bool `json:"enabled"` 6 | Endpoint string `json:"endpoint,omitempty"` 7 | ServingEndpoint string `json:"servingEndpoint,omitempty"` 8 | AccessKey string `json:"accessKey,omitempty"` 9 | Secret string `json:"secret,omitempty"` 10 | Bucket string `json:"bucket,omitempty"` 11 | Region string `json:"region,omitempty"` 12 | ACL string `json:"acl,omitempty"` 13 | ForcePathStyle bool `json:"forcePathStyle"` 14 | } 15 | -------------------------------------------------------------------------------- /owncast/models/stats.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/owncast/owncast/utils" 5 | ) 6 | 7 | // Stats holds the stats for the system. 8 | type Stats struct { 9 | SessionMaxViewerCount int `json:"sessionMaxViewerCount"` 10 | OverallMaxViewerCount int `json:"overallMaxViewerCount"` 11 | LastDisconnectTime *utils.NullTime `json:"lastDisconnectTime"` 12 | 13 | StreamConnected bool `json:"-"` 14 | LastConnectTime *utils.NullTime `json:"-"` 15 | ChatClients map[string]Client `json:"-"` 16 | Viewers map[string]*Viewer `json:"-"` 17 | } 18 | -------------------------------------------------------------------------------- /owncast/models/status.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/owncast/owncast/utils" 4 | 5 | // Status represents the status of the system. 6 | type Status struct { 7 | Online bool `json:"online"` 8 | ViewerCount int `json:"viewerCount"` 9 | OverallMaxViewerCount int `json:"overallMaxViewerCount"` 10 | SessionMaxViewerCount int `json:"sessionMaxViewerCount"` 11 | 12 | LastConnectTime *utils.NullTime `json:"lastConnectTime"` 13 | LastDisconnectTime *utils.NullTime `json:"lastDisconnectTime"` 14 | 15 | VersionNumber string `json:"versionNumber"` 16 | StreamTitle string `json:"streamTitle"` 17 | } 18 | -------------------------------------------------------------------------------- /owncast/models/storageProvider.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // StorageProvider is how a chunk storage provider should be implemented. 4 | type StorageProvider interface { 5 | Setup() error 6 | Save(filePath string, retryCount int) (string, error) 7 | 8 | SegmentWritten(localFilePath string) 9 | VariantPlaylistWritten(localFilePath string) 10 | MasterPlaylistWritten(localFilePath string) 11 | } 12 | -------------------------------------------------------------------------------- /owncast/models/streamHealth.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // StreamHealthOverview represents an overview of the current stream health. 4 | type StreamHealthOverview struct { 5 | Healthy bool `json:"healthy"` 6 | HealthyPercentage int `json:"healthPercentage"` 7 | Message string `json:"message"` 8 | Representation int `json:"representation"` 9 | } 10 | -------------------------------------------------------------------------------- /owncast/models/userJoinedEvent.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | // UserJoinedEvent represents an event when a user joins the chat. 6 | type UserJoinedEvent struct { 7 | Username string `json:"username"` 8 | Type EventType `json:"type"` 9 | ID string `json:"id"` 10 | Timestamp time.Time `json:"timestamp,omitempty"` 11 | } 12 | -------------------------------------------------------------------------------- /owncast/models/viewer.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/owncast/owncast/geoip" 8 | "github.com/owncast/owncast/utils" 9 | ) 10 | 11 | // Viewer represents a single video viewer. 12 | type Viewer struct { 13 | FirstSeen time.Time `json:"firstSeen"` 14 | LastSeen time.Time `json:"-"` 15 | UserAgent string `json:"userAgent"` 16 | IPAddress string `json:"ipAddress"` 17 | ClientID string `json:"clientID"` 18 | Geo *geoip.GeoDetails `json:"geo"` 19 | } 20 | 21 | // GenerateViewerFromRequest will return a chat client from a http request. 22 | func GenerateViewerFromRequest(req *http.Request) Viewer { 23 | return Viewer{ 24 | FirstSeen: time.Now(), 25 | LastSeen: time.Now(), 26 | UserAgent: req.UserAgent(), 27 | IPAddress: utils.GetIPAddressFromRequest(req), 28 | ClientID: utils.GenerateClientIDFromRequest(req), 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /owncast/models/webhook.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/owncast/owncast/utils" 7 | ) 8 | 9 | // Webhook is an event that is sent to 3rd party, external services with details about something that took place within an Owncast server. 10 | type Webhook struct { 11 | ID int `json:"id"` 12 | URL string `json:"url"` 13 | Events []EventType `json:"events"` 14 | Timestamp time.Time `json:"timestamp"` 15 | LastUsed *time.Time `json:"lastUsed"` 16 | } 17 | 18 | // For an event to be seen as "valid" it must live in this slice. 19 | var validEvents = []EventType{ 20 | MessageSent, 21 | UserJoined, 22 | UserNameChanged, 23 | VisibiltyToggled, 24 | StreamStarted, 25 | StreamStopped, 26 | } 27 | 28 | // HasValidEvents will verify that all the events provided are valid. 29 | // This is not a efficient method. 30 | func HasValidEvents(events []EventType) bool { 31 | for _, event := range events { 32 | if _, foundInSlice := utils.FindInSlice(validEvents, event); !foundInSlice { 33 | return false 34 | } 35 | } 36 | return true 37 | } 38 | -------------------------------------------------------------------------------- /owncast/notifications/channels.go: -------------------------------------------------------------------------------- 1 | package notifications 2 | 3 | const ( 4 | // BrowserPushNotification represents a push notification for a browser. 5 | BrowserPushNotification = "BROWSER_PUSH_NOTIFICATION" 6 | ) 7 | -------------------------------------------------------------------------------- /owncast/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "baseBranches": [ 6 | "develop", 7 | "webv2" 8 | ], 9 | "timezone": "America/Los_Angeles", 10 | "lockFileMaintenance": { 11 | "enabled": true, 12 | "automerge": true, 13 | "platformAutomerge": true 14 | }, 15 | "npm": { 16 | "stabilityDays": 3 17 | }, 18 | "dependencyDashboard": true, 19 | "major": { 20 | "dependencyDashboardApproval": true 21 | }, 22 | "packageRules": [ 23 | { 24 | "description": "Automatically merge minor and patch-level updates", 25 | "matchUpdateTypes": [ 26 | "minor", 27 | "patch", 28 | "digest" 29 | ], 30 | "automerge": true, 31 | "automergeType": "branch", 32 | "platformAutomerge": true, 33 | "dependencyDashboardApproval": false 34 | }, 35 | { 36 | "description": "Require approval for every Go language update", 37 | "dependencyDashboardApproval": true, 38 | "matchPackagePatterns": [ 39 | "go" 40 | ] 41 | }, 42 | { 43 | "description": "Ignore the old pre-0.1.0 web packages", 44 | "matchPackageNames": [ 45 | "postcss", 46 | "tailwindcss", 47 | "cssnano", 48 | "htm", 49 | "mark.js", 50 | "postcss-cli", 51 | "@videojs/themes", 52 | "@joeattardi/emoji-button", 53 | "preact" 54 | ], 55 | "enabled": false 56 | } 57 | ] 58 | } -------------------------------------------------------------------------------- /owncast/router/middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // EnableCors enables the CORS header on the responses. 8 | func EnableCors(w http.ResponseWriter) { 9 | w.Header().Set("Access-Control-Allow-Origin", "*") 10 | } 11 | -------------------------------------------------------------------------------- /owncast/router/middleware/headers.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | // SetHeaders will set our global headers for web resources. 11 | func SetHeaders(w http.w) { 12 | // When running automated browser tests we must allow `unsafe-eval` in our CSP 13 | // so we can explicitly add it only when needed. 14 | inTest := os.Getenv("BROWSER_TEST") == "true" 15 | unsafeEval := "" 16 | if inTest { 17 | unsafeEval = `'unsafe-eval'` 18 | } 19 | // Content security policy 20 | csp := []string{ 21 | fmt.Sprintf("script-src 'self' %s 'sha256-B5bOgtE39ax4J6RqDE93TVYrJeLAdxDOJFtF3hoWYDw=' 'sha256-PzXGlTLvNFZ7et6GkP2nD3XuSaAKQVBSYiHzU2ZKm8o=' 'sha256-/wqazZOqIpFSIrNVseblbKCXrezG73X7CMqRSTf+8zw=' 'sha256-jCj2f+ICtd8fvdb0ngc+Hkr/ZnZOMvNkikno/XR6VZs='", unsafeEval), 22 | "worker-src 'self' blob:", // No single quotes around blob: 23 | } 24 | w.Header().Set("Content-Security-Policy", strings.Join(csp, "; ")) 25 | w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") 26 | w.Header().Set("Pragma", "no-cache") 27 | w.Header().Set("Expires", "0") 28 | } 29 | -------------------------------------------------------------------------------- /owncast/router/middleware/pagination.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | ) 7 | 8 | // PaginatedHandlerFunc is a handler for endpoints that require pagination. 9 | type PaginatedHandlerFunc func(int, int, http.ResponseWriter, *http.Request) 10 | 11 | // HandlePagination is a middleware handler that pulls pagination values 12 | // and passes them along. 13 | func HandlePagination(handler PaginatedHandlerFunc) http.HandlerFunc { 14 | return func(w http.ResponseWriter, r *http.Request) { 15 | // Default 50 items per page 16 | limitString := r.URL.Query().Get("limit") 17 | if limitString == "" { 18 | limitString = "50" 19 | } 20 | limit, err := strconv.Atoi(limitString) 21 | if err != nil { 22 | w.WriteHeader(http.StatusBadRequest) 23 | return 24 | } 25 | 26 | // Default first page 0 27 | offsetString := r.URL.Query().Get("offset") 28 | if offsetString == "" { 29 | offsetString = "0" 30 | } 31 | offset, err := strconv.Atoi(offsetString) 32 | if err != nil { 33 | w.WriteHeader(http.StatusBadRequest) 34 | return 35 | } 36 | 37 | handler(offset, limit, w, r) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /owncast/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | packages: 3 | - path: db 4 | name: db 5 | schema: 'db/schema.sql' 6 | queries: 'db/query.sql' 7 | -------------------------------------------------------------------------------- /owncast/static/admin/_next/static/IF0q45tHTUnbAhOX9pFCQ/_middlewareManifest.js: -------------------------------------------------------------------------------- 1 | self.__MIDDLEWARE_MANIFEST=[];self.__MIDDLEWARE_MANIFEST_CB&&self.__MIDDLEWARE_MANIFEST_CB() -------------------------------------------------------------------------------- /owncast/static/admin/_next/static/IF0q45tHTUnbAhOX9pFCQ/_ssgManifest.js: -------------------------------------------------------------------------------- 1 | self.__SSG_MANIFEST=new Set,self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB(); -------------------------------------------------------------------------------- /owncast/static/admin/_next/static/chunks/pages/_error-785557186902809b.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[4820],{14977:function(n,_,u){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_error",function(){return u(89185)}])}},function(n){n.O(0,[9774,2888,179],(function(){return _=14977,n(n.s=_);var _}));var _=n.O();_N_E=_}]); -------------------------------------------------------------------------------- /owncast/static/admin/_next/static/chunks/pages/config-social-items-42e2ed4eed8d4dd2.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[4269],{48689:function(e,n,t){"use strict";t.d(n,{Z:function(){return a}});var c=t(1413),i=t(67294),r={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M360 184h-8c4.4 0 8-3.6 8-8v8h304v-8c0 4.4 3.6 8 8 8h-8v72h72v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80h72v-72zm504 72H160c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60.4l24.7 523c1.6 34.1 29.8 61 63.9 61h454c34.2 0 62.3-26.8 63.9-61l24.7-523H888c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM731.3 840H292.7l-24.2-512h487l-24.2 512z"}}]},name:"delete",theme:"outlined"},s=t(42135),u=function(e,n){return i.createElement(s.Z,(0,c.Z)((0,c.Z)({},e),{},{ref:n,icon:r}))};u.displayName="DeleteOutlined";var a=i.forwardRef(u)},23999:function(e,n,t){(window.__NEXT_P=window.__NEXT_P||[]).push(["/config-social-items",function(){return t(57535)}])},57535:function(e,n,t){"use strict";t.r(n),t.d(n,{default:function(){return u}});var c=t(85893),i=(t(67294),t(84485)),r=t(91017),s=i.Z.Title;function u(){return(0,c.jsxs)("div",{className:"config-social-items",children:[(0,c.jsx)(s,{children:"Social Items"}),(0,c.jsx)(r.Z,{})]})}}},function(e){e.O(0,[1741,6003,1017,9774,2888,179],(function(){return n=23999,e(e.s=n);var n}));var n=e.O();_N_E=n}]); -------------------------------------------------------------------------------- /owncast/static/admin/fediverse-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/static/admin/fediverse-white.png -------------------------------------------------------------------------------- /owncast/static/offline.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/static/offline.ts -------------------------------------------------------------------------------- /owncast/static/static.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import ( 4 | "embed" 5 | "html/template" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | //go:embed admin/* 11 | //go:embed admin/_next/static 12 | //go:embed admin/_next/static/chunks/pages/*.js 13 | //go:embed admin/_next/static/*/*.js 14 | var adminFiles embed.FS 15 | 16 | // GetAdmin will return an embedded filesystem reference to the admin web app. 17 | func GetAdmin() embed.FS { 18 | return adminFiles 19 | } 20 | 21 | //go:embed metadata.html.tmpl 22 | var botMetadataTemplate embed.FS 23 | 24 | // GetBotMetadataTemplate will return the bot/scraper metadata template. 25 | func GetBotMetadataTemplate() (*template.Template, error) { 26 | name := "metadata.html.tmpl" 27 | t, err := template.ParseFS(botMetadataTemplate, name) 28 | tmpl := template.Must(t, err) 29 | return tmpl, err 30 | } 31 | 32 | //go:embed offline.ts 33 | var offlineVideoSegment []byte 34 | 35 | // GetOfflineSegment will return the offline video segment data. 36 | func GetOfflineSegment() []byte { 37 | return getFileSystemStaticFileOrDefault("offline.ts", offlineVideoSegment) 38 | } 39 | 40 | func getFileSystemStaticFileOrDefault(path string, defaultData []byte) []byte { 41 | fullPath := filepath.Join("static", path) 42 | data, err := os.ReadFile(fullPath) //nolint: gosec 43 | if err != nil { 44 | return defaultData 45 | } 46 | 47 | return data 48 | } 49 | -------------------------------------------------------------------------------- /owncast/test/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /owncast/test/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | ## Load Tests 4 | 5 | 1. Install [artillery](https://artillery.io/) from NPM/Yarn/Whatever Javascript package manager is popular this week. 6 | 1. Start an instance of the server on localhost. 7 | 1. `artillery run httpGetTest.yaml` for endpoint load tests. 8 | 1. `artillery run websocketTest.yaml` for websocket load tests. 9 | 10 | 11 | ## Chat test 12 | 13 | This will send automated fake chat messages to your localhost instance. 14 | Edit the messages, usernames or point to a different instance. 15 | 16 | 1. `npm install` 17 | 1. `node fakeChat.js` 18 | -------------------------------------------------------------------------------- /owncast/test/automated/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | ffmpeg 3 | -------------------------------------------------------------------------------- /owncast/test/automated/api/admin.test.js: -------------------------------------------------------------------------------- 1 | var request = require('supertest'); 2 | request = request('http://127.0.0.1:8080'); 3 | 4 | 5 | 6 | test('correct number of log entries exist', (done) => { 7 | request.get('/api/admin/logs').auth('admin', 'abc123').expect(200) 8 | .then((res) => { 9 | // expect(res.body).toHaveLength(8); 10 | done(); 11 | }); 12 | }); 13 | 14 | -------------------------------------------------------------------------------- /owncast/test/automated/api/index.test.js: -------------------------------------------------------------------------------- 1 | var request = require('supertest'); 2 | request = request('http://127.0.0.1:8080'); 3 | 4 | test('service is online', (done) => { 5 | request.get('/api/status').expect(200) 6 | .then((res) => { 7 | expect(res.body.online).toBe(true); 8 | done(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /owncast/test/automated/api/lib/chat.js: -------------------------------------------------------------------------------- 1 | var request = require('supertest'); 2 | request = request('http://127.0.0.1:8080'); 3 | const WebSocket = require('ws'); 4 | 5 | async function registerChat() { 6 | try { 7 | const response = await request.post('/api/chat/register'); 8 | return response.body; 9 | } catch (e) { 10 | console.error(e); 11 | } 12 | } 13 | 14 | async function sendChatMessage(message, accessToken, done) { 15 | const ws = new WebSocket(`ws://localhost:8080/ws?accessToken=${accessToken}`); 16 | 17 | async function onOpen() { 18 | ws.send(JSON.stringify(message), async function () { 19 | ws.close(); 20 | done(); 21 | }); 22 | } 23 | 24 | ws.on('open', onOpen); 25 | } 26 | 27 | async function listenForEvent(name, accessToken, done) { 28 | const ws = new WebSocket(`ws://localhost:8080/ws?accessToken=${accessToken}`); 29 | 30 | ws.on('message', function incoming(message) { 31 | const messages = message.split('\n'); 32 | messages.forEach(function (message) { 33 | const event = JSON.parse(message); 34 | 35 | if (event.type === name) { 36 | done(); 37 | ws.close(); 38 | } 39 | }); 40 | }); 41 | } 42 | 43 | module.exports.sendChatMessage = sendChatMessage; 44 | module.exports.registerChat = registerChat; 45 | module.exports.listenForEvent = listenForEvent; 46 | -------------------------------------------------------------------------------- /owncast/test/automated/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owncast-test-automation", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "supertest": "^6.0.1", 13 | "websocket": "^1.0.32" 14 | }, 15 | "devDependencies": { 16 | "jest": "^26.6.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /owncast/test/automated/api/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TEMP_DB=$(mktemp) 4 | 5 | # Install the node test framework 6 | npm install --silent > /dev/null 7 | 8 | # Download a specific version of ffmpeg 9 | if [ ! -d "ffmpeg" ]; then 10 | mkdir ffmpeg 11 | pushd ffmpeg > /dev/null 12 | curl -sL https://github.com/vot/ffbinaries-prebuilt/releases/download/v4.2.1/ffmpeg-4.2.1-linux-64.zip --output ffmpeg.zip > /dev/null 13 | unzip -o ffmpeg.zip > /dev/null 14 | PATH=$PATH:$(pwd) 15 | popd > /dev/null 16 | fi 17 | 18 | pushd ../../.. > /dev/null 19 | 20 | # Build and run owncast from source 21 | go build -race -o owncast main.go 22 | ./owncast -database $TEMP_DB & 23 | SERVER_PID=$! 24 | 25 | popd > /dev/null 26 | sleep 5 27 | 28 | # Start streaming the test file over RTMP to 29 | # the local owncast instance. 30 | ffmpeg -hide_banner -loglevel panic -stream_loop -1 -re -i ../test.mp4 -vcodec libx264 -profile:v main -sc_threshold 0 -b:v 1300k -acodec copy -f flv rtmp://127.0.0.1/live/abc123 & 31 | FFMPEG_PID=$! 32 | 33 | function finish { 34 | rm $TEMP_DB 35 | kill $SERVER_PID $FFMPEG_PID 36 | } 37 | trap finish EXIT 38 | 39 | echo "Waiting..." 40 | sleep 15 41 | 42 | # Run the tests against the instance. 43 | npm test 44 | -------------------------------------------------------------------------------- /owncast/test/automated/browser/README.md: -------------------------------------------------------------------------------- 1 | # Automated browser tests 2 | 3 | The tests currently address the following interfaces: 4 | 5 | 1. The main web frontend of Owncast 6 | 1. The embeddable video player 7 | 1. The embeddable read-only chat 8 | 1. the embeddable read-write chat 9 | 10 | Each have a set of test to make sure they load, have the expected elements on the screen, that API requests are successful, and that there are no errors being thrown in the console. 11 | 12 | The main web frontend additionally iterates its tests over a set of different device characteristics to verify mobile and tablet usage and goes through some interactive usage of the page such as changing their name and sending a chat message by clicking and typing. 13 | 14 | While it emulates the user agent, screen size, and touch features of different devices, they're still just a copy of Chromium running and not a true emulation of these other devices. So any "it breaks only on Safari" type bugs will not get caught. 15 | 16 | It can't actually play video, so anything specific about video playback cannot be verified with these tests. 17 | 18 | ## Setup 19 | 20 | `npm install` 21 | 22 | ## Run 23 | 24 | `./run.sh` 25 | ## Screenshots 26 | 27 | After the tests finish a set of screenshots will be saved into the `screenshots` directory to aid in troubleshooting or sanity checking different viewport sizes. three -------------------------------------------------------------------------------- /owncast/test/automated/browser/admin.test.js: -------------------------------------------------------------------------------- 1 | const listenForErrors = require('./lib/errors.js').listenForErrors; 2 | const ADMIN_USERNAME = 'admin'; 3 | const ADMIN_PASSWORD = 'abc123'; 4 | 5 | describe('Admin page', () => { 6 | beforeAll(async () => { 7 | await page.setViewport({ width: 1080, height: 720 }); 8 | listenForErrors(browser, page); 9 | 10 | // set HTTP Basic auth 11 | await page.authenticate({ 12 | username: ADMIN_USERNAME, 13 | password: ADMIN_PASSWORD, 14 | }); 15 | 16 | await page.goto('http://localhost:5309/admin'); 17 | }); 18 | 19 | afterAll(async () => { 20 | await page.waitForTimeout(3000); 21 | await page.screenshot({ path: 'screenshots/admin.png', fullPage: true }); 22 | }); 23 | 24 | it('should have rendered the admin home page', async () => { 25 | await page.waitForSelector('.home-container'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /owncast/test/automated/browser/chat-embed.test.js: -------------------------------------------------------------------------------- 1 | const listenForErrors = require('./lib/errors.js').listenForErrors; 2 | const interactiveChatTest = require('./tests/chat.js').interactiveChatTest; 3 | 4 | describe('Chat read-write embed page', () => { 5 | beforeAll(async () => { 6 | await page.setViewport({ width: 600, height: 700 }); 7 | listenForErrors(browser, page); 8 | await page.goto('http://localhost:5309/embed/chat/readwrite'); 9 | }); 10 | 11 | afterAll(async () => { 12 | await page.waitForTimeout(3000); 13 | await page.screenshot({ path: 'screenshots/screenshot_chat_embed.png', fullPage: true }); 14 | }); 15 | 16 | const newName = 'frontend-browser-embed-test-name-change'; 17 | const fakeMessage = 'this is a test chat message sent via the automated browser tests on the read/write chat embed page.' 18 | 19 | interactiveChatTest(browser, page, newName, fakeMessage, 'desktop'); 20 | }); 21 | 22 | describe('Chat read-only embed page', () => { 23 | beforeAll(async () => { 24 | await page.setViewport({ width: 500, height: 700 }); 25 | listenForErrors(browser, page); 26 | await page.goto('http://localhost:5309/embed/chat/readonly'); 27 | }); 28 | 29 | it('should have the messages container', async () => { 30 | await page.waitForSelector('#messages-container'); 31 | }); 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /owncast/test/automated/browser/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owncast browser tests", 3 | "preset": "jest-puppeteer" 4 | } 5 | -------------------------------------------------------------------------------- /owncast/test/automated/browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owncast-browser-tests", 3 | "version": "1.0.0", 4 | "description": "Automated browser testing for Owncast", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/owncast/owncast.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/owncast/owncast/issues" 17 | }, 18 | "homepage": "https://github.com/owncast/owncast#readme", 19 | "devDependencies": { 20 | "jest": "^27.2.0", 21 | "jest-puppeteer": "^5.0.4", 22 | "puppeteer": "^9.1.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /owncast/test/automated/browser/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TEMP_DB=$(mktemp) 4 | 5 | # Install the node test framework 6 | npm install --silent > /dev/null 7 | 8 | # Download a specific version of ffmpeg 9 | if [ ! -d "ffmpeg" ]; then 10 | mkdir ffmpeg 11 | pushd ffmpeg > /dev/null 12 | curl -sL https://github.com/vot/ffbinaries-prebuilt/releases/download/v4.2.1/ffmpeg-4.2.1-linux-64.zip --output ffmpeg.zip > /dev/null 13 | unzip -o ffmpeg.zip > /dev/null 14 | PATH=$PATH:$(pwd) 15 | popd > /dev/null 16 | fi 17 | 18 | pushd ../../.. > /dev/null 19 | 20 | # Build and run owncast from source 21 | go build -o owncast main.go 22 | BROWSER_TEST=true ./owncast -rtmpport 9021 -webserverport 5309 -database $TEMP_DB & 23 | SERVER_PID=$! 24 | 25 | popd > /dev/null 26 | sleep 5 27 | 28 | # Start streaming the test file over RTMP to 29 | # the local owncast instance. 30 | ffmpeg -hide_banner -loglevel panic -stream_loop -1 -re -i ../test.mp4 -vcodec libx264 -profile:v main -sc_threshold 0 -b:v 1300k -acodec copy -f flv rtmp://127.0.0.1:9021/live/abc123 & 31 | FFMPEG_PID=$! 32 | 33 | function finish { 34 | rm $TEMP_DB 35 | kill $SERVER_PID $FFMPEG_PID 36 | } 37 | trap finish EXIT 38 | 39 | echo "Waiting..." 40 | sleep 15 41 | 42 | # Run the tests against the instance. 43 | npm test -------------------------------------------------------------------------------- /owncast/test/automated/browser/screenshots/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/test/automated/browser/screenshots/.gitkeep -------------------------------------------------------------------------------- /owncast/test/automated/browser/tests/chat.js: -------------------------------------------------------------------------------- 1 | async function interactiveChatTest( 2 | browser, 3 | page, 4 | newName, 5 | chatMessage, 6 | device 7 | ) { 8 | it('should have the chat input', async () => { 9 | await page.waitForSelector('#message-input'); 10 | }); 11 | 12 | it('should have the chat input enabled', async () => { 13 | const isDisabled = await page.evaluate( 14 | 'document.querySelector("#message-input").getAttribute("disabled")' 15 | ); 16 | expect(isDisabled).not.toBe('true'); 17 | }); 18 | 19 | it('should allow typing a chat message', async () => { 20 | await page.waitForSelector('#message-input'); 21 | await page.evaluate(() => document.querySelector('#message-input').click()); 22 | await page.waitForTimeout(1000); 23 | await page.focus('#message-input'); 24 | await page.keyboard.type(chatMessage); 25 | page.keyboard.press('Enter'); 26 | }); 27 | } 28 | 29 | module.exports.interactiveChatTest = interactiveChatTest; 30 | -------------------------------------------------------------------------------- /owncast/test/automated/browser/tests/video.js: -------------------------------------------------------------------------------- 1 | async function videoTest(browser, page) { 2 | it('should have the video container element', async () => { 3 | await page.waitForSelector('#video-container'); 4 | }); 5 | 6 | it('should have the stream info status bar', async () => { 7 | await page.waitForSelector('#stream-info'); 8 | }); 9 | } 10 | 11 | module.exports.videoTest = videoTest; 12 | -------------------------------------------------------------------------------- /owncast/test/automated/browser/video-embed.test.js: -------------------------------------------------------------------------------- 1 | const listenForErrors = require('./lib/errors.js').listenForErrors; 2 | const videoTest = require('./tests/video.js').videoTest; 3 | 4 | describe('Video embed page', () => { 5 | beforeAll(async () => { 6 | await page.setViewport({ width: 1080, height: 720 }); 7 | listenForErrors(browser, page); 8 | await page.goto('http://localhost:5309/embed/video'); 9 | }); 10 | 11 | afterAll(async () => { 12 | await page.waitForTimeout(3000); 13 | await page.screenshot({ path: 'screenshots/screenshot_video_embed.png', fullPage: true }); 14 | }); 15 | 16 | videoTest(browser, page); 17 | }); 18 | -------------------------------------------------------------------------------- /owncast/test/automated/hls/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owncast-test-automation", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest --bail" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "m3u8-parser": "^4.7.0", 13 | "node-fetch": "^2.6.7" 14 | }, 15 | "devDependencies": { 16 | "jest": "^26.6.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /owncast/test/automated/test.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/test/automated/test.mp4 -------------------------------------------------------------------------------- /owncast/test/load/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | ## Load Tests 4 | 5 | 1. Install [artillery](https://artillery.io/) from NPM/Yarn/Whatever Javascript package manager is popular this week. 6 | 1. Install artillery-plugin-hls 7 | 1. Start an instance of the server on localhost. 8 | 1. `artillery run httpGetTest.yaml` for endpoint load tests. 9 | 1. `artillery run websocketTest.yaml` for websocket load tests. 10 | 11 | -------------------------------------------------------------------------------- /owncast/test/load/hls.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | target: 'http://localhost:8080' 3 | phases: 4 | - duration: 3600 5 | arrivalRate: 500 6 | plugins: 7 | hls: {} 8 | 9 | scenarios: 10 | - name: Test the actual HLS video stream 11 | flow: 12 | - get: 13 | url: "/hls/0/stream.m3u8" 14 | hls: 15 | concurrency: 4 16 | streamSelector: 17 | index: all -------------------------------------------------------------------------------- /owncast/test/load/httpGetTest.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | target: 'http://localhost:8080/' 3 | 4 | ensure: 5 | p95: 200 6 | maxErrorRate: 1 7 | 8 | phases: 9 | - duration: 60 10 | arrivalRate: 20 11 | scenarios: 12 | - flow: 13 | - get: 14 | url: "/hls/stream.m3u8" 15 | - get: 16 | url: "/status" -------------------------------------------------------------------------------- /owncast/test/load/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owncast-load-tests", 3 | "version": "1.0.0", 4 | "description": "## Load Tests", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "artillery": "^1.7.2", 13 | "ws": "^7.4.6" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /owncast/test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "node-fetch": "^2.6.7", 4 | "ws": "^8.2.3" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /owncast/utils/accessTokens.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | ) 7 | 8 | const tokenLength = 32 9 | 10 | // GenerateAccessToken will generate and return an access token. 11 | func GenerateAccessToken() (string, error) { 12 | return generateRandomString(tokenLength) 13 | } 14 | 15 | // generateRandomBytes returns securely generated random bytes. 16 | // It will return an error if the system's secure random 17 | // number generator fails to function correctly, in which 18 | // case the caller should not continue. 19 | func generateRandomBytes(n int) ([]byte, error) { 20 | b := make([]byte, n) 21 | _, err := rand.Read(b) 22 | // Note that err == nil only if we read len(b) bytes. 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | return b, nil 28 | } 29 | 30 | // generateRandomString returns a URL-safe, base64 encoded 31 | // securely generated random string. 32 | // It will return an error if the system's secure random 33 | // number generator fails to function correctly, in which 34 | // case the caller should not continue. 35 | func generateRandomString(n int) (string, error) { 36 | b, err := generateRandomBytes(n) 37 | return base64.URLEncoding.EncodeToString(b), err 38 | } 39 | -------------------------------------------------------------------------------- /owncast/utils/accessTokens_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCreateAccessToken(t *testing.T) { 8 | if _, err := GenerateAccessToken(); err != nil { 9 | t.Error(err) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /owncast/utils/clientId.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/md5" //nolint 5 | "encoding/hex" 6 | "net" 7 | "net/http" 8 | 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // GenerateClientIDFromRequest generates a client id from the provided request. 13 | func GenerateClientIDFromRequest(req *http.Request) string { 14 | ipAddress := GetIPAddressFromRequest(req) 15 | clientID := ipAddress + req.UserAgent() 16 | 17 | // Create a MD5 hash of this ip + useragent 18 | b := md5.Sum([]byte(clientID)) // nolint 19 | return hex.EncodeToString(b[:]) 20 | } 21 | 22 | // GetIPAddressFromRequest returns the IP address from a http request. 23 | func GetIPAddressFromRequest(req *http.Request) string { 24 | ipAddressString := req.RemoteAddr 25 | xForwardedFor := req.Header.Get("X-FORWARDED-FOR") 26 | if xForwardedFor != "" { 27 | return xForwardedFor 28 | } 29 | 30 | ip, _, err := net.SplitHostPort(ipAddressString) 31 | if err != nil { 32 | log.Errorln(err) 33 | return "" 34 | } 35 | 36 | return ip 37 | } 38 | -------------------------------------------------------------------------------- /owncast/utils/nulltime.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | // NullTime is a custom nullable time for representing datetime. 10 | type NullTime struct { 11 | Time time.Time 12 | Valid bool // Valid is true if Time is not NULL 13 | } 14 | 15 | // Scan implements the Scanner interface. 16 | func (nt *NullTime) Scan(value interface{}) error { 17 | nt.Time, nt.Valid = value.(time.Time) 18 | return nil 19 | } 20 | 21 | // Value implements the driver Value interface. 22 | func (nt NullTime) Value() (driver.Value, error) { 23 | if !nt.Valid { 24 | return nil, nil 25 | } 26 | return nt.Time, nil 27 | } 28 | 29 | // MarshalJSON implements the JSON marshal function. 30 | func (nt NullTime) MarshalJSON() ([]byte, error) { 31 | if !nt.Valid { 32 | return []byte("null"), nil 33 | } 34 | val := fmt.Sprintf("\"%s\"", nt.Time.Format(time.RFC3339)) 35 | return []byte(val), nil 36 | } 37 | 38 | // UnmarshalJSON implements the JSON unmarshal function. 39 | func (nt NullTime) UnmarshalJSON(data []byte) error { 40 | dateString := string(data) 41 | if dateString == "null" { 42 | return nil 43 | } 44 | 45 | dateStringWithoutQuotes := dateString[1 : len(dateString)-1] 46 | parsedDateTime, err := time.Parse(time.RFC3339, dateStringWithoutQuotes) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | nt.Time = parsedDateTime // nolint 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /owncast/utils/restendpointhelper_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestGetPatternForRestEndpoint(t *testing.T) { 9 | expected := "/hello/" 10 | endpoints := [...]string{"/hello/{param1}", "/hello/{param1}/{param2}", "/hello/{param1}/world/{param2}"} 11 | for _, endpoint := range endpoints { 12 | if ep := getPatternForRestEndpoint(endpoint); ep != expected { 13 | t.Errorf("%s p does not match expected %s", ep, expected) 14 | } 15 | } 16 | } 17 | 18 | func TestReadParameter(t *testing.T) { 19 | expected := "world" 20 | endpoints := [...]string{ 21 | "/hello/{p1}", 22 | "/hello/cruel/{p1}", 23 | "/hello/{p1}/my/friend", 24 | "/hello/{p1}/{p2}/friend", 25 | "/hello/{p2}/{p3}/{p1}", 26 | "/{p1}/is/nice", 27 | "/{p1}/{p1}/{p1}", 28 | } 29 | 30 | for _, ep := range endpoints { 31 | v, err := readParameter(ep, strings.Replace(ep, "{p1}", expected, -1), "p1") 32 | if err != nil { 33 | t.Errorf("Unexpected error when reading parameter: %s", err.Error()) 34 | } 35 | if v != expected { 36 | t.Errorf("'%s' should have returned %s", ep, expected) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /owncast/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestUserAgent(t *testing.T) { 8 | testAgents := []string{ 9 | "Pleroma 1.0.0-1168-ge18c7866-pleroma-dot-site; https://pleroma.site info@pleroma.site", 10 | "Mastodon 1.2.3 Bot", 11 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Safari/605.1.15 (Applebot/0.1; +http://www.apple.com/go/applebot)", 12 | "WhatsApp", 13 | } 14 | 15 | for _, agent := range testAgents { 16 | if !IsUserAgentABot(agent) { 17 | t.Error("Incorrect parsing of useragent", agent) 18 | } 19 | } 20 | } 21 | 22 | func TestGetHashtagsFromText(t *testing.T) { 23 | text := `Some text with a #hashtag goes here.\n\n 24 | Another #secondhashtag, goes here.\n\n 25 | #thirdhashtag` 26 | 27 | hashtags := GetHashtagsFromText(text) 28 | 29 | if hashtags[0] != "#hashtag" || hashtags[1] != "#secondhashtag" || hashtags[2] != "#thirdhashtag" { 30 | t.Error("Incorrect hashtags fetched from text.") 31 | } 32 | } 33 | 34 | func TestPercentageUtilsTest(t *testing.T) { 35 | total := 42 36 | number := 18 37 | 38 | percent := IntPercentage(number, total) 39 | 40 | if percent != 42 { 41 | t.Error("Incorrect percentage calculation.") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /owncast/webroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/favicon.ico -------------------------------------------------------------------------------- /owncast/webroot/img/airplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/airplay.png -------------------------------------------------------------------------------- /owncast/webroot/img/ban-user-grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/browser-push-notifications-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/browser-push-notifications-settings.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/Reaper-gg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/Reaper-gg.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/Reaper-hi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/Reaper-hi.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/Reaper-hype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/Reaper-hype.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/Reaper-lol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/Reaper-lol.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/Reaper-love.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/Reaper-love.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/Reaper-rage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/Reaper-rage.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/Reaper-rip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/Reaper-rip.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/Reaper-wtf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/Reaper-wtf.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/ac-box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/ac-box.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/ac-construction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/ac-construction.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/ac-fossil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/ac-fossil.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/ac-item-leaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/ac-item-leaf.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/ac-kkslider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/ac-kkslider.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/ac-moneytree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/ac-moneytree.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/ac-mosquito.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/ac-mosquito.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/ac-shirt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/ac-shirt.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/ac-song.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/ac-song.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/ac-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/ac-tree.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/ac-turnip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/ac-turnip.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/ac-weeds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/ac-weeds.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/alert.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/alert.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/bananadance.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/bananadance.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/bb8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/bb8.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/beerparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/beerparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/bells.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/bells.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/birthdaypartyparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/birthdaypartyparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/blacklightsaber.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/blacklightsaber.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/bluelightsaber.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/bluelightsaber.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/bluntparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/bluntparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/bobaparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/bobaparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/cakeparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/cakeparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/chewbacca.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/chewbacca.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/chillparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/chillparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/christmasparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/christmasparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/coffeeparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/coffeeparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/confusedparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/confusedparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/copparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/copparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/coronavirus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/coronavirus.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/covid19parrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/covid19parrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/cryptoparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/cryptoparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/dabparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/dabparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/dadparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/dadparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/daftpunkparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/daftpunkparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/darkbeerparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/darkbeerparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/darkmodeparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/darkmodeparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/darth_vader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/darth_vader.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/dealwithitparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/dealwithitparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/death_star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/death_star.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/discoparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/discoparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/division-gg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/division-gg.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/division-hi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/division-hi.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/division-hype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/division-hype.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/division-lol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/division-lol.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/division-omg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/division-omg.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/division-rage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/division-rage.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/division-rip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/division-rip.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/division-wtf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/division-wtf.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/docparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/docparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/donutparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/donutparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/doom_mad.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/doom_mad.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/empire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/empire.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/everythingsfineparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/everythingsfineparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/evilparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/evilparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/explodyparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/explodyparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/fixparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/fixparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/flyingmoneyparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/flyingmoneyparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/footballparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/footballparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/gabe1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/gabe1.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/gabe2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/gabe2.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/gentlemanparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/gentlemanparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/githubparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/githubparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/goomba.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/goomba.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/gothparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/gothparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/hamburgerparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/hamburgerparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/harrypotterparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/harrypotterparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/headbangingparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/headbangingparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/headingparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/headingparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/headsetparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/headsetparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/hmmparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/hmmparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/hypnoparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/hypnoparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/icecreamparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/icecreamparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/illuminatiparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/illuminatiparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/jediparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/jediparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/keanu_thanks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/keanu_thanks.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/laptop_parrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/laptop_parrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/loveparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/loveparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/mandalorian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/mandalorian.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/margaritaparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/margaritaparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/mario.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/mario.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/matrixparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/matrixparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/meldparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/meldparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/metalparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/metalparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/michaeljacksonparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/michaeljacksonparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/moonparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/moonparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/moonwalkingparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/moonwalkingparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/mustacheparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/mustacheparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/nicolas_cage_party.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/nicolas_cage_party.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/nodeparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/nodeparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/norwegianblueparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/norwegianblueparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/opensourceparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/opensourceparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/originalparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/originalparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/owncast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/owncast.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/palpatine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/palpatine.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/papalparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/papalparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/parrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/parrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/parrotnotfound.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/parrotnotfound.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/partyparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/partyparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/phparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/phparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/pirateparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/pirateparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/pizzaparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/pizzaparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/pokeparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/pokeparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/popcornparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/popcornparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/porg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/porg.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/portalparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/portalparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/pumpkinparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/pumpkinparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/quadparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/quadparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/r2d2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/r2d2.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/redenvelopeparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/redenvelopeparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/ripparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/ripparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/rotatingparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/rotatingparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/ryangoslingparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/ryangoslingparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/rythmicalparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/rythmicalparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/sadparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/sadparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/schnitzelparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/schnitzelparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/scienceparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/scienceparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/shipitparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/shipitparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/shufflepartyparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/shufflepartyparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/sintparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/sintparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/sithparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/sithparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/skiparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/skiparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/sleepingparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/sleepingparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/sonic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/sonic.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/spyparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/spyparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/stalkerparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/stalkerparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/starwars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/starwars.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/stayhomeparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/stayhomeparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/storm_trooper.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/storm_trooper.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/stormtrooper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/stormtrooper.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/sushiparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/sushiparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/tacoparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/tacoparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/tennisparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/tennisparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/thanks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/thanks.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/thumbsupparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/thumbsupparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/tiedyeparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/tiedyeparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/tpparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/tpparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/transparront.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/transparront.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/twinsparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/twinsparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/upvoteparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/upvoteparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/vikingparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/vikingparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/wesmart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/wesmart.png -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/wfhparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/wfhparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/wineparrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/wineparrot.gif -------------------------------------------------------------------------------- /owncast/webroot/img/emoji/yoda.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/emoji/yoda.gif -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/android-icon-144x144.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/android-icon-192x192.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/android-icon-36x36.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/android-icon-48x48.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/android-icon-72x72.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/android-icon-96x96.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/apple-icon-114x114.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/apple-icon-120x120.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/apple-icon-144x144.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/apple-icon-152x152.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/apple-icon-180x180.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/apple-icon-57x57.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/apple-icon-60x60.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/apple-icon-72x72.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/apple-icon-76x76.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/apple-icon-precomposed.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/apple-icon.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/ms-icon-144x144.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/ms-icon-150x150.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/ms-icon-310x310.png -------------------------------------------------------------------------------- /owncast/webroot/img/favicon/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/favicon/ms-icon-70x70.png -------------------------------------------------------------------------------- /owncast/webroot/img/fediverse-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/fediverse-black.png -------------------------------------------------------------------------------- /owncast/webroot/img/fediverse-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/fediverse-color.png -------------------------------------------------------------------------------- /owncast/webroot/img/fediverse-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/fediverse-white.png -------------------------------------------------------------------------------- /owncast/webroot/img/hide-message-grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/indieauth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/indieauth.png -------------------------------------------------------------------------------- /owncast/webroot/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/loading.gif -------------------------------------------------------------------------------- /owncast/webroot/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/logo.png -------------------------------------------------------------------------------- /owncast/webroot/img/menu-filled.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/menu-vert.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/moderator-grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/moderator-nobackground.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/moderator.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/notification-bell.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/owncast-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/owncast-background.png -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/bandcamp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/discord.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/facebook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/gitlab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/google.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/keyoxide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/platformlogos/keyoxide.png -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/ko-fi.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/lbry.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/liberapay.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/linkedin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/mastodon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/matrix.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/patreon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/paypal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/snapchat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/spotify.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/tiktok.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/twitch.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/platformlogos/youtube.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/img/smiley.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/webroot/img/smiley.png -------------------------------------------------------------------------------- /owncast/webroot/img/user-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /owncast/webroot/index-standalone-chat-readonly.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /owncast/webroot/index-standalone-chat-readwrite.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /owncast/webroot/index-standalone-chat.html: -------------------------------------------------------------------------------- 1 | index-standalone-chat-readonly.html -------------------------------------------------------------------------------- /owncast/webroot/index-video-only.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /owncast/webroot/js/chat/register.js: -------------------------------------------------------------------------------- 1 | import { URL_CHAT_REGISTRATION } from '../utils/constants.js'; 2 | 3 | export async function registerChat(username) { 4 | const options = { 5 | method: 'POST', 6 | headers: { 7 | 'Content-Type': 'application/json', 8 | }, 9 | body: JSON.stringify({ displayName: username }), 10 | }; 11 | 12 | try { 13 | const response = await fetch(URL_CHAT_REGISTRATION, options); 14 | const result = await response.json(); 15 | return result; 16 | } catch (e) { 17 | console.error(e); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /owncast/webroot/js/components/icons/AuthIcon.js: -------------------------------------------------------------------------------- 1 | import { h } from '/js/web_modules/preact.js'; 2 | import htm from '/js/web_modules/htm.js'; 3 | const html = htm.bind(h); 4 | 5 | export const AuthIcon = ({ className }) => { 6 | return html` 7 | 14 | 20 | 21 | `; 22 | }; 23 | -------------------------------------------------------------------------------- /owncast/webroot/js/components/icons/CaretDownIcon.js: -------------------------------------------------------------------------------- 1 | import { h } from '/js/web_modules/preact.js'; 2 | import htm from '/js/web_modules/htm.js'; 3 | const html = htm.bind(h); 4 | 5 | export const CaretDownIcon = ({ className = 'w-6 h-6' }) => { 6 | return html` 12 | 17 | `; 18 | }; 19 | -------------------------------------------------------------------------------- /owncast/webroot/js/components/icons/ChatIcon.js: -------------------------------------------------------------------------------- 1 | import { h } from '/js/web_modules/preact.js'; 2 | import htm from '/js/web_modules/htm.js'; 3 | const html = htm.bind(h); 4 | 5 | export const ChatIcon = ({ className = 'w-6 h-6' }) => { 6 | return html` 7 | 13 | 18 | 19 | `; 20 | }; 21 | -------------------------------------------------------------------------------- /owncast/webroot/js/components/icons/CheckIcon.js: -------------------------------------------------------------------------------- 1 | import { h } from '/js/web_modules/preact.js'; 2 | import htm from '/js/web_modules/htm.js'; 3 | const html = htm.bind(h); 4 | 5 | export const CheckIcon = ({ className = 'w-6 h-6' }) => { 6 | return html` 13 | 19 | `; 20 | }; 21 | -------------------------------------------------------------------------------- /owncast/webroot/js/components/icons/CloseIcon.js: -------------------------------------------------------------------------------- 1 | import { h } from '/js/web_modules/preact.js'; 2 | import htm from '/js/web_modules/htm.js'; 3 | const html = htm.bind(h); 4 | 5 | export const CloseIcon = ({ className = 'w-6 h-6' }) => { 6 | return html` 12 | 17 | `; 18 | }; 19 | -------------------------------------------------------------------------------- /owncast/webroot/js/components/icons/EditIcon.js: -------------------------------------------------------------------------------- 1 | import { h } from '/js/web_modules/preact.js'; 2 | import htm from '/js/web_modules/htm.js'; 3 | const html = htm.bind(h); 4 | 5 | export const EditIcon = ({ className = 'w-6 h-6' }) => { 6 | return html` 12 | 15 | `; 16 | }; 17 | -------------------------------------------------------------------------------- /owncast/webroot/js/components/icons/UserIcon.js: -------------------------------------------------------------------------------- 1 | import { h } from '/js/web_modules/preact.js'; 2 | import htm from '/js/web_modules/htm.js'; 3 | const html = htm.bind(h); 4 | 5 | export const UserIcon = ({ className }) => { 6 | return html` 7 | 14 | 20 | 21 | `; 22 | }; 23 | -------------------------------------------------------------------------------- /owncast/webroot/js/components/icons/index.js: -------------------------------------------------------------------------------- 1 | export { ChatIcon } from './ChatIcon.js'; 2 | export { UserIcon } from './UserIcon.js'; 3 | export { EditIcon } from './EditIcon.js'; 4 | export { CheckIcon } from './CheckIcon.js'; 5 | export { CloseIcon } from './CloseIcon.js'; 6 | export { CaretDownIcon } from './CaretDownIcon.js'; 7 | export { AuthIcon } from './AuthIcon.js'; 8 | -------------------------------------------------------------------------------- /owncast/webroot/js/notification/registerWeb.js: -------------------------------------------------------------------------------- 1 | export async function registerWebPushNotifications(vapidPublicKey) { 2 | const registration = await navigator.serviceWorker.ready; 3 | let subscription = await registration.pushManager.getSubscription(); 4 | 5 | if (!subscription) { 6 | subscription = await registration.pushManager.subscribe({ 7 | userVisibleOnly: true, 8 | applicationServerKey: urlBase64ToUint8Array(vapidPublicKey), 9 | }); 10 | } 11 | 12 | return JSON.stringify(subscription); 13 | } 14 | 15 | export function isPushNotificationSupported() { 16 | return 'serviceWorker' in navigator && 'PushManager' in window; 17 | } 18 | 19 | function urlBase64ToUint8Array(base64String) { 20 | var padding = '='.repeat((4 - (base64String.length % 4)) % 4); 21 | var base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/'); 22 | 23 | var rawData = window.atob(base64); 24 | var outputArray = new Uint8Array(rawData.length); 25 | 26 | for (var i = 0; i < rawData.length; ++i) { 27 | outputArray[i] = rawData.charCodeAt(i); 28 | } 29 | return outputArray; 30 | } 31 | -------------------------------------------------------------------------------- /owncast/webroot/js/utils/user-colors.js: -------------------------------------------------------------------------------- 1 | export function messageBubbleColorForHue(hue) { 2 | // Tweak these to adjust the result of the color 3 | const saturation = 50; 4 | const lightness = 50; 5 | const alpha = 'var(--message-background-alpha)'; 6 | 7 | return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`; 8 | } 9 | 10 | export function textColorForHue(hue) { 11 | // Tweak these to adjust the result of the color 12 | const saturation = 70; 13 | const lightness = 80; 14 | const alpha = 0.85; 15 | 16 | return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`; 17 | } 18 | -------------------------------------------------------------------------------- /owncast/webroot/js/web_modules/common/_commonjsHelpers-8c19dec8.js: -------------------------------------------------------------------------------- 1 | var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 2 | 3 | function getDefaultExportFromCjs (x) { 4 | return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; 5 | } 6 | 7 | function createCommonjsModule(fn, basedir, module) { 8 | return module = { 9 | path: basedir, 10 | exports: {}, 11 | require: function (path, base) { 12 | return commonjsRequire(path, (base === undefined || base === null) ? module.path : base); 13 | } 14 | }, fn(module, module.exports), module.exports; 15 | } 16 | 17 | function commonjsRequire () { 18 | throw new Error('Dynamic requires are not currently supported by @rollup/plugin-commonjs'); 19 | } 20 | 21 | export { commonjsGlobal as a, createCommonjsModule as c, getDefaultExportFromCjs as g }; 22 | -------------------------------------------------------------------------------- /owncast/webroot/js/web_modules/htm.js: -------------------------------------------------------------------------------- 1 | var n=function(t,s,r,e){var u;s[0]=0;for(var h=1;h=5&&((e||!n&&5===r)&&(h.push(r,0,e,s),r=6),n&&(h.push(r,n,0,s),r=6)),e="";},a=0;a"===t?(r=1,e=""):e=t+e[0]:u?t===u?u="":e+=t:'"'===t||"'"===t?u=t:">"===t?(p(),r=1):r&&("="===t?(r=5,s=e,e=""):"/"===t&&(r<5||">"===n[a][l+1])?(p(),3===r&&(h=h[0]),r=h,(h=h[0]).push(2,0,r),r=0):" "===t||"\t"===t||"\n"===t||"\r"===t?(p(),r=2):e+=t),3===r&&"!--"===e&&(r=4,h=h[0]);}return p(),h}(s)),r),arguments,[])).length>1?r:r[0]} 2 | 3 | export { htm_module as default }; 4 | -------------------------------------------------------------------------------- /owncast/webroot/js/web_modules/import-map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "@joeattardi/emoji-button": "./@joeattardi/emoji-button.js", 4 | "@videojs/themes/fantasy/index.css": "./@videojs/themes/fantasy/index.css", 5 | "htm": "./htm.js", 6 | "mark.js/dist/mark.es6.min.js": "./markjs/dist/mark.es6.min.js", 7 | "micromodal/dist/micromodal.min.js": "./micromodal/dist/micromodal.min.js", 8 | "preact": "./preact.js", 9 | "preact/hooks": "./preact/hooks.js", 10 | "tailwindcss/dist/tailwind.min.css": "./tailwindcss/dist/tailwind.min.css", 11 | "video.js/dist/video-js.min.css": "./videojs/dist/video-js.min.css", 12 | "video.js/dist/video.min.js": "./videojs/dist/video.min.js" 13 | } 14 | } -------------------------------------------------------------------------------- /owncast/webroot/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Owncast", 3 | "icons": [ 4 | { 5 | "src": "\/img\/favicon\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/img\/favicon\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/img\/favicon\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/img\/favicon\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/img\/favicon\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/img\/favicon\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ], 41 | "display": "fullscreen" 42 | } 43 | -------------------------------------------------------------------------------- /owncast/webroot/serviceWorker.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('activate', (event) => { 2 | console.log('Owncast service worker activated', event); 3 | }); 4 | 5 | self.addEventListener('install', (event) => { 6 | console.log('installing Owncast service worker...', event); 7 | }); 8 | 9 | self.addEventListener('push', (event) => { 10 | const data = JSON.parse(event.data.text()); 11 | const { title, body, icon, tag } = data; 12 | const options = { 13 | title: title || 'Live!', 14 | body: body || 'This live stream has started.', 15 | icon: icon || '/logo/external', 16 | tag: tag, 17 | }; 18 | 19 | event.waitUntil(self.registration.showNotification(options.title, options)); 20 | }); 21 | 22 | self.addEventListener('notificationclick', (event) => { 23 | clients.openWindow('/'); 24 | }); 25 | -------------------------------------------------------------------------------- /owncast/webroot/styles/standalone-chat-readwrite.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --header-height: 2em; 3 | } 4 | 5 | header{ 6 | height: var(--header-height); 7 | } 8 | 9 | #messages-container { 10 | height: 100vh; 11 | } 12 | 13 | #chat-container { 14 | width: 100%; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /owncast/webroot/styles/video-only.css: -------------------------------------------------------------------------------- 1 | /* 2 | The styles in this file mostly override those coming from chat.css 3 | */ 4 | 5 | /* modify this px number if you want things to be relatively bigger or smaller */ 6 | #video-only { 7 | font-size: 16px; 8 | position: relative; 9 | } 10 | 11 | 12 | #video-only #video-container { 13 | background-size: 30%; 14 | width: 100%; 15 | height: calc((9 / 16) * 100vw); 16 | position: relative; 17 | } 18 | #video-only #video-container #video { 19 | transition: opacity .5s; 20 | opacity: 0; 21 | pointer-events: none; 22 | } 23 | #video-only .online #video-container #video { 24 | opacity: 1; 25 | pointer-events: auto; 26 | } 27 | -------------------------------------------------------------------------------- /owncast/yp/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkatyan/bbb-streaming/f1b958778524f447c029142ff3aef01d56c26464/owncast/yp/README.md -------------------------------------------------------------------------------- /restart-streaming.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Restarting streaming..." 4 | 5 | bash stop-streaming.sh 6 | 7 | bash start-streaming.sh -------------------------------------------------------------------------------- /sample-env: -------------------------------------------------------------------------------- 1 | BBB_URL=https://example.com/bigbluebutton/ 2 | 3 | BBB_SECRET=super_secret 4 | 5 | MEETING_ID=bbb-meeting-id 6 | 7 | MEETING_PASSWORD= 8 | 9 | #RTMP url format will be rtmp:/// 10 | RTMP_URL=rtmp://example.com/live2/your_streaming_key 11 | 12 | SHOW_PRESENTATION=true 13 | 14 | STREAMING_RESOLUTION=1920x1080 -------------------------------------------------------------------------------- /start-streaming.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | test ! -f ./env && echo "Unable to find the env file" && echo "No such file $(pwd)/env" && exit 0 4 | 5 | set -a 6 | source <(cat ./env | \ 7 | sed -e '/^#/d;/^\s*$/d' -e "s/'/'\\\''/g" -e "s/=\(.*\)/='\1'/g") 8 | set +a 9 | 10 | 11 | if [ -z $BBB_URL ]; then 12 | echo "Error: BBB_URL cannot be empty" 13 | exit 0 14 | fi 15 | if [ -z $BBB_SECRET ]; then 16 | echo "Error: BBB_SECRET cannot be empty" 17 | exit 0 18 | fi 19 | if [ -z $MEETING_ID ]; then 20 | echo "Error: MEETING_ID cannot be empty" 21 | exit 0 22 | fi 23 | 24 | if [ -z $RTMP_URL ]; then 25 | echo "Error: RTMP_URL cannot be empty" 26 | exit 0 27 | fi 28 | if [ -z $SHOW_PRESENTATION ]; then 29 | echo "Error: SHOW_PRESENTATION cannot be empty" 30 | exit 0 31 | fi 32 | 33 | docker run --rm -d\ 34 | --name bbb-streaming \ 35 | --env-file $(pwd)/env \ 36 | manishkatyan/bbb-streaming:v2.5.1 37 | -------------------------------------------------------------------------------- /stop-streaming.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | streaming_container=bbb-streaming 4 | 5 | echo "Stopping streaming.." 6 | docker kill $streaming_container --------------------------------------------------------------------------------