├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
├── pull_request_template.md
└── workflows
│ ├── codeql-analysis.yml
│ ├── deploy-docs.yaml
│ ├── deploy-rtmp-proxy.yml
│ ├── deploy-runners.yml
│ ├── deploy-server.yml
│ ├── deploy-workers.yml
│ ├── eslint.yml
│ ├── go-test.yml
│ ├── golangci-lint.yml
│ └── pr-opened-notification.yml
├── .gitignore
├── .golangci.yml
├── .idea
├── .gitignore
├── TUM-Live-Backend.iml
├── codeStyles
│ └── codeStyleConfig.xml
├── dataSources.xml
├── jsLibraryMappings.xml
├── jsLinters
│ └── eslint.xml
├── modules.xml
├── prettier.xml
├── protoeditor.xml
├── runConfigurations
│ ├── TUM_Live__frontend__worker.xml
│ ├── build_dev.xml
│ ├── build_production.xml
│ ├── clean.xml
│ ├── lint.xml
│ ├── lint_fix.xml
│ ├── postinstall.xml
│ ├── run_dev.xml
│ ├── run_production.xml
│ ├── run_tumlive_go.xml
│ └── tailwind_compile.xml
├── sqldialects.xml
├── vcs.xml
└── webResources.xml
├── .pre-commit-config.yaml
├── CNAME
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── SECURITY.md
├── api
├── api_logger.go
├── audit.go
├── audit_test.go
├── bookmarks.go
├── bookmarks_test.go
├── chat.go
├── chat_test.go
├── courseimport.go
├── courses.go
├── courses_test.go
├── download.go
├── download_ics.go
├── download_ics_test.go
├── download_test.go
├── info-pages.go
├── info-pages_test.go
├── lecture_halls.go
├── lecture_halls_test.go
├── live_update.go
├── maintenance.go
├── notifications.go
├── notifications_test.go
├── progress.go
├── progress_test.go
├── realtime.go
├── router.go
├── runner.go
├── search.go
├── search_test.go
├── seek_stats.go
├── seek_stats_test.go
├── semesters.go
├── server-notifications.go
├── statistics.go
├── statistics_test.go
├── stream.go
├── stream_test.go
├── template
│ ├── ical.gotemplate
│ ├── ics.gotemplate
│ └── mail-course-registered.gotemplate
├── token.go
├── token_test.go
├── users.go
├── users_test.go
├── voice_service_grpc.go
├── worker.go
├── worker_grpc.go
├── worker_test.go
└── wsHub.go
├── apiv2
├── README.md
├── buf.gen.yaml
├── buf.lock
├── buf.yaml
├── errors
│ └── errors.go
├── generate.sh
├── google
│ └── api
│ │ ├── annotations.proto
│ │ ├── http.proto
│ │ └── httpbody.proto
├── helpers
│ ├── parser.go
│ └── queries.go
├── installBuf.sh
├── protobuf
│ └── server
│ │ ├── apiv2.pb.go
│ │ ├── apiv2.pb.gw.go
│ │ └── apiv2_grpc.pb.go
├── protoc-gen-openapiv2
│ └── options
│ │ ├── annotations.proto
│ │ └── openapiv2.proto
└── server
│ ├── api.go
│ ├── apiv2.proto
│ ├── authorization.go
│ ├── bookmark.go
│ ├── course.go
│ ├── docs
│ ├── apiv2.swagger.json
│ ├── index.html
│ ├── swagger-ui-bundle.js
│ ├── swagger-ui.css
│ └── swagger.json
│ ├── notification.go
│ ├── progress.go
│ ├── semester.go
│ ├── stream.go
│ └── user.go
├── branding
├── branding.yaml
├── favicon.ico
├── icons-192.png
├── icons-512.png
├── logo.svg
├── manifest.json
└── thumb-fallback.png
├── cmd
├── modelGen
│ ├── daoTemplate.tmpl
│ ├── modelGen.go
│ └── modelTemplate.tmpl
└── tumlive
│ └── tumlive.go
├── config.yaml
├── dao
├── audit.go
├── bookmark.go
├── cache.go
├── camera-preset.go
├── chat.go
├── courses.go
├── dao_base.go
├── dao_logger.go
├── email.go
├── file.go
├── info-page.go
├── ingest_server.go
├── lecture_halls.go
├── migrations
│ ├── 202201280.go
│ ├── 202207240.go
│ ├── 202208100.go
│ ├── 202208110.go
│ ├── 202210270.go
│ ├── 202212010.go
│ ├── 202212020.go
│ └── 202301006.go
├── migrator.go
├── notifications.go
├── progress.go
├── runner.go
├── server-notification.go
├── statistics.go
├── streams.go
├── subtitles.go
├── token.go
├── transcoding-failure.go
├── upload_key.go
├── users.go
├── video-section.go
├── video-seek.go
└── worker.go
├── docker-compose.yml
├── docs
├── .gitignore
├── docs
│ ├── chat-img
│ │ ├── activate.jpg
│ │ ├── anonymous.jpg
│ │ ├── approve.jpg
│ │ ├── at-ing.jpg
│ │ ├── dismiss.jpg
│ │ ├── emojis.jpg
│ │ ├── poll-result.jpg
│ │ ├── polls.jpg
│ │ ├── resolve.jpg
│ │ └── resolved-with-mark.jpg
│ ├── chat.md
│ ├── course-nav.png
│ ├── frontend-components.md
│ ├── index.md
│ ├── lecture-edit.png
│ ├── new-course-prompt.png
│ ├── new-course-prompt.png.kra
│ ├── new-course.png
│ ├── user-guide.md
│ ├── video-img
│ │ ├── sections-on-watch-page.jpg
│ │ └── video-sections.jpg
│ └── video.md
├── mkdocs.yml
├── requirements.txt
├── static
│ └── tum-live-starter.sql
└── styling
│ ├── colors.md
│ ├── components.md
│ └── img
│ ├── dark_form.jpg
│ ├── dark_input.jpg
│ ├── dark_primary_button.jpg
│ ├── dark_secondary_buttons.jpg
│ ├── light_form.jpg
│ ├── light_input.jpg
│ ├── light_primary_button.jpg
│ └── light_secondary_buttons.jpg
├── docs_v2
├── .gitignore
├── LICENSE
├── README.md
├── babel.config.js
├── blog
│ └── changelogs
│ │ ├── v1.2.26.md
│ │ ├── v1.2.27.md
│ │ └── v1.4.18.md
├── docs
│ ├── features
│ │ ├── LectureHallStreams.md
│ │ ├── VideoOnDemand.md
│ │ ├── _category_.json
│ │ └── _index.md
│ ├── intro.md
│ ├── selfhosting
│ │ ├── _category_.json
│ │ ├── deploy.md
│ │ ├── networking.md
│ │ ├── prerequisites.md
│ │ └── setup-docker.md
│ └── usage
│ │ ├── _category_.json
│ │ ├── chat-img
│ │ ├── activate.jpg
│ │ ├── anonymous.jpg
│ │ ├── approve.jpg
│ │ ├── at-ing.jpg
│ │ ├── dismiss.jpg
│ │ ├── emojis.jpg
│ │ ├── poll-result.jpg
│ │ ├── polls.jpg
│ │ ├── resolve.jpg
│ │ └── resolved-with-mark.jpg
│ │ ├── chat.md
│ │ ├── course-img
│ │ ├── course-nav.png
│ │ ├── lecture-edit.png
│ │ ├── new-course-prompt.png
│ │ └── new-course.png
│ │ ├── lecturehall-streaming.md
│ │ ├── self-streaming.md
│ │ ├── user-guide.md
│ │ ├── video-img
│ │ ├── sections-on-watch-page.jpg
│ │ └── video-sections.jpg
│ │ └── video.md
├── docusaurus.config.js
├── package-lock.json
├── package.json
├── sidebars.js
├── src
│ ├── components
│ │ └── HomepageFeatures
│ │ │ ├── index.js
│ │ │ └── styles.module.css
│ ├── css
│ │ └── custom.css
│ ├── pages
│ │ ├── index.js
│ │ ├── index.module.css
│ │ └── markdown-page.md
│ └── prism-include-languages.js
├── static
│ ├── deployment
│ │ ├── DeploymentDiagram.png
│ │ ├── config.yaml
│ │ ├── docker-compose.yml
│ │ ├── runners.png
│ │ ├── traefik.toml
│ │ └── workers.png
│ ├── icons
│ │ ├── clapperboard.svg
│ │ ├── curves.svg
│ │ ├── film-camera.svg
│ │ ├── gocast-gopher-lg.png
│ │ ├── gocast-gopher-xs.png
│ │ ├── play.svg
│ │ ├── server.svg
│ │ └── tum-live-logo.svg
│ └── img
│ │ ├── showcase-01.png
│ │ ├── showcase-02.png
│ │ └── showcase-03.png
├── versioned_docs
│ └── version-beta
│ │ ├── deployment
│ │ ├── _category_.json
│ │ ├── deploy-with-docker-swarm.mdx
│ │ ├── overview.md
│ │ ├── prerequisites.md
│ │ ├── step-by-step
│ │ │ ├── _category_.json
│ │ │ ├── add-runner.mdx
│ │ │ ├── add-vodservice.mdx
│ │ │ ├── add-worker.mdx
│ │ │ ├── example-deployment.md
│ │ │ └── setup-edge.mdx
│ │ └── troubleshooting.md
│ │ ├── features
│ │ ├── LectureHallStreams.md
│ │ ├── Organizations.md
│ │ ├── VideoOnDemand.md
│ │ ├── _category_.json
│ │ └── _index.md
│ │ ├── intro.md
│ │ ├── selfhosting
│ │ ├── _category_.json
│ │ ├── deploy.md
│ │ ├── networking.md
│ │ ├── prerequisites.md
│ │ └── setup-docker.md
│ │ └── usage
│ │ ├── _category_.json
│ │ ├── chat-img
│ │ ├── activate.jpg
│ │ ├── anonymous.jpg
│ │ ├── approve.jpg
│ │ ├── at-ing.jpg
│ │ ├── dismiss.jpg
│ │ ├── emojis.jpg
│ │ ├── poll-result.jpg
│ │ ├── polls.jpg
│ │ ├── resolve.jpg
│ │ └── resolved-with-mark.jpg
│ │ ├── chat.md
│ │ ├── course-img
│ │ ├── course-nav.png
│ │ ├── lecture-edit.png
│ │ ├── new-course-prompt.png
│ │ └── new-course.png
│ │ ├── lecturehall-streaming.md
│ │ ├── self-streaming.mdx
│ │ ├── user-guide.md
│ │ ├── video-img
│ │ ├── sections-on-watch-page.jpg
│ │ └── video-sections.jpg
│ │ └── video.md
├── versioned_sidebars
│ └── version-beta-sidebars.json
└── versions.json
├── go.mod
├── go.sum
├── go.work
├── go.work.sum
├── init.sql
├── mock_dao
├── audit.go
├── bookmark.go
├── camera-preset.go
├── chat.go
├── courses.go
├── email.go
├── file.go
├── info-page.go
├── ingest_server.go
├── lecture_halls.go
├── notifications.go
├── progress.go
├── runner.go
├── server-notification.go
├── statistics.go
├── streams.go
├── subtitles.go
├── token.go
├── transcoding-failure.go
├── upload_key.go
├── users.go
├── video-section.go
├── video-seek.go
└── worker.go
├── mock_tools
├── meiliSearch.go
├── mock_camera
│ └── camera.go
└── presets.go
├── model
├── audit.go
├── bookmark.go
├── camera_preset.go
├── chat.go
├── chat_reaction.go
├── chat_test.go
├── course.go
├── course_test.go
├── email.go
├── file.go
├── info-page.go
├── ingest-server.go
├── lecture_hall.go
├── lecture_hall_test.go
├── model-base.go
├── model_logger.go
├── notification.go
├── poll.go
├── progress.go
├── register_link.go
├── runner.go
├── search
│ ├── doc.go
│ └── prefetchedCourse.go
├── semester.go
├── server-notification.go
├── shortlink.go
├── silence.go
├── stat.go
├── stream-name.go
├── stream-unit.go
├── stream.go
├── subtitles.go
├── token.go
├── transcoding-failure.go
├── transcodingProgress.go
├── upload-key.go
├── user.go
├── video-section.go
├── video-seek-chunk.go
└── worker.go
├── persist.gob
├── pkg
└── runner_manager
│ ├── manager.go
│ └── manager_test.go
├── renovate.json
├── rtmp-proxy
├── Dockerfile
├── README.md
├── nginx.conf
└── publish.sh
├── runner
├── .gitignore
├── Dockerfile
├── Makefile
├── cleanup.go
├── cmd
│ └── runner
│ │ └── main.go
├── commons.proto
├── config
│ └── config.go
├── entrypoint.sh
├── go.mod
├── go.sum
├── handlers.go
├── hls.go
├── mediamtx.yml
├── notifications.proto
├── pkg
│ ├── actions
│ │ ├── actions.go
│ │ ├── checkvod.go
│ │ ├── error.go
│ │ ├── error_test.go
│ │ ├── mkvod.go
│ │ ├── mkvod_test.go
│ │ ├── stream.go
│ │ └── stream_end.go
│ ├── ffmpeg
│ │ └── ffmpeg.go
│ ├── metrics
│ │ ├── broker.go
│ │ └── broker_test.go
│ ├── netutil
│ │ └── netutil.go
│ ├── ptr
│ │ └── ptr.go
│ └── vmstat
│ │ └── vmstat.go
├── protobuf
│ ├── commons.pb.go
│ ├── notifications.pb.go
│ ├── runner.pb.go
│ └── runner_grpc.pb.go
├── runner.go
├── runner.proto
└── serverhandler.go
├── tools
├── api-errors.go
├── bot
│ ├── bot.go
│ ├── bot_logger.go
│ └── matrix.go
├── branding.go
├── cache.go
├── camera
│ ├── axis.go
│ ├── camera.go
│ ├── camera_logger.go
│ └── panasonic.go
├── canonical.go
├── config.go
├── cron.go
├── email.go
├── functions.go
├── functions_test.go
├── json-generator.go
├── meiliExporter.go
├── meiliSearch.go
├── middlewares.go
├── pathprovider
│ └── pathprovider.go
├── presets.go
├── realtime
│ ├── channel.go
│ ├── channel_store.go
│ ├── channel_store_test.go
│ ├── channel_test.go
│ ├── client-store.go
│ ├── client.go
│ ├── connector.go
│ ├── connector
│ │ └── melody.go
│ ├── context.go
│ ├── realtime.go
│ ├── realtime_logger.go
│ ├── realtime_test.go
│ └── subscribers.go
├── session.go
├── stats_export.dart.go
├── stream-signing.go
├── template_executor.go
├── testutils
│ ├── testdata.go
│ └── testutils.go
├── timing
│ ├── time.go
│ └── time_test.go
├── tools_logger.go
├── truncate.go
└── tum
│ ├── campus-online-base.go
│ ├── courses.go
│ ├── events.go
│ ├── ldap.go
│ ├── students.go
│ └── tum_logger.go
├── tumlive.example.service
├── vod-service
├── Dockerfile
├── README.md
├── cmd
│ └── vod-service
│ │ └── main.go
├── go.mod
└── internal
│ ├── internal_logger.go
│ └── vodService.go
├── voice-service
├── pb
│ ├── subtitles.pb.go
│ └── subtitles_grpc.pb.go
└── subtitles.proto
├── web
├── .eslintrc
├── .prettierrc.js
├── admin.go
├── assets
│ ├── css
│ │ ├── home.css
│ │ ├── icons.css
│ │ ├── main.css
│ │ └── watch.css
│ ├── favicon.ico
│ ├── img
│ │ ├── icons-192.png
│ │ ├── icons-512.png
│ │ ├── logo.svg
│ │ └── thumb-fallback.png
│ ├── init-admin.js
│ ├── init.js
│ ├── manifest.json
│ └── service-worker.js
├── course.go
├── index.go
├── package-lock.json
├── package.json
├── popup.go
├── router.go
├── saml.go
├── tailwind.config.js
├── template
│ ├── admin
│ │ ├── admin.gohtml
│ │ ├── admin_tabs
│ │ │ ├── audits.gohtml
│ │ │ ├── course-import.gohtml
│ │ │ ├── create-course.gohtml
│ │ │ ├── create-lecture_halls.gohtml
│ │ │ ├── edit-course.gohtml
│ │ │ ├── info-pages.gohtml
│ │ │ ├── lecture_halls.gohtml
│ │ │ ├── maintenance.gohtml
│ │ │ ├── notifications.gohtml
│ │ │ ├── runners.gohtml
│ │ │ ├── schedule.gohtml
│ │ │ ├── server-notifications.gohtml
│ │ │ ├── stats.gohtml
│ │ │ ├── token.gohtml
│ │ │ ├── users.gohtml
│ │ │ └── workers.gohtml
│ │ ├── lecture-cut.gohtml
│ │ ├── lecture-live-management.gohtml
│ │ ├── lecture-stats.gohtml
│ │ ├── lecture-units.gohtml
│ │ └── opt-out.gohtml
│ ├── components
│ │ └── chat.gohtml
│ ├── course-card.gohtml
│ ├── course-overview.gohtml
│ ├── course_list.gohtml
│ ├── edit-course-by-token.gohtml
│ ├── error.gohtml
│ ├── headImports.gohtml
│ ├── header.gohtml
│ ├── home.gohtml
│ ├── index.gohtml
│ ├── info-page.gohtml
│ ├── login.gohtml
│ ├── onboarding.gohtml
│ ├── partial
│ │ ├── close-btn.gohtml
│ │ ├── course
│ │ │ └── manage
│ │ │ │ ├── camera-presets.gohtml
│ │ │ │ ├── course-admin-management.gohtml
│ │ │ │ ├── course-lecture-management.gohtml
│ │ │ │ ├── course-settings-modal.gohtml
│ │ │ │ ├── course_actions.gohtml
│ │ │ │ ├── course_settings.gohtml
│ │ │ │ ├── create-lecture-form-slides
│ │ │ │ ├── lecture-details-slide.gohtml
│ │ │ │ ├── lecture-media-slide.gohtml
│ │ │ │ ├── lecture-record-slide.gohtml
│ │ │ │ ├── lecture-type-slide.gohtml
│ │ │ │ └── livestream-type-slide.gohtml
│ │ │ │ ├── create-lecture-form.gohtml
│ │ │ │ ├── edit-video-sections.gohtml
│ │ │ │ ├── external-participants.gohtml
│ │ │ │ ├── lecture-hall-settings.gohtml
│ │ │ │ ├── lecture-management-card.gohtml
│ │ │ │ └── semester-selection.gohtml
│ │ ├── download.gohtml
│ │ ├── footer.gohtml
│ │ ├── info-dropdown.gohtml
│ │ ├── shortcuts.gohtml
│ │ ├── stream
│ │ │ ├── actions.gohtml
│ │ │ ├── alert-modal.gohtml
│ │ │ ├── attachments.gohtml
│ │ │ ├── bookmarks.gohtml
│ │ │ ├── playlist.gohtml
│ │ │ ├── search.gohtml
│ │ │ ├── transcript.gohtml
│ │ │ ├── video-sections.gohtml
│ │ │ └── watch-info.gohtml
│ │ ├── terminalprompt.gohtml
│ │ ├── theme-selector-head.gohtml
│ │ └── theme-selector.gohtml
│ ├── passwordreset.gohtml
│ ├── planned_course_list.gohtml
│ ├── popup-chat.gohtml
│ ├── reload-page-button.gohtml
│ ├── search-global.gohtml
│ ├── search-page.gohtml
│ ├── semester-selection.gohtml
│ ├── user-settings.gohtml
│ ├── video_only.gohtml
│ ├── vod_course_list.gohtml
│ └── watch.gohtml
├── ts
│ ├── TUMLiveVjs.ts
│ ├── admin.ts
│ ├── api
│ │ ├── Identifiable.ts
│ │ ├── admin-lecture-list.ts
│ │ ├── chat-ws.ts
│ │ ├── chat.ts
│ │ ├── courses.ts
│ │ ├── notifications.ts
│ │ ├── poll-ws.ts
│ │ ├── progress.ts
│ │ ├── runner.ts
│ │ ├── semesters.ts
│ │ ├── users.ts
│ │ ├── video-sections.ts
│ │ └── watched.ts
│ ├── audits.ts
│ ├── bookmarks.ts
│ ├── change-set.ts
│ ├── chat
│ │ ├── ChatMessagePreprocessor.ts
│ │ ├── ChatMessageSorter.ts
│ │ ├── EmojiPicker.ts
│ │ └── misc.ts
│ ├── components
│ │ ├── alpine-component.ts
│ │ ├── chat-prompt.ts
│ │ ├── chat.ts
│ │ ├── course.ts
│ │ ├── emoji-picker.ts
│ │ ├── header.ts
│ │ ├── livestreams.ts
│ │ ├── main.ts
│ │ ├── poll.ts
│ │ ├── popup.ts
│ │ ├── servernotifications.ts
│ │ ├── video-information.ts
│ │ ├── video-interaction.ts
│ │ └── video-sections.ts
│ ├── course-import.ts
│ ├── course-overview.ts
│ ├── courseAdminManagement.ts
│ ├── create-course.ts
│ ├── custom-elements
│ │ ├── elements.ts
│ │ └── help-icon.ts
│ ├── data-store
│ │ ├── admin-lecture-list.ts
│ │ ├── bookmarks.ts
│ │ ├── cache.ts
│ │ ├── data-store.ts
│ │ ├── provider.ts
│ │ ├── stream-playlist.ts
│ │ └── video-sections.ts
│ ├── edit-course.ts
│ ├── entry
│ │ ├── admins.ts
│ │ ├── home.ts
│ │ ├── interactions.ts
│ │ ├── user.ts
│ │ └── video.ts
│ ├── global.ts
│ ├── hotkeys.ts
│ ├── interval-updates.ts
│ ├── lecture-hall-management.ts
│ ├── lecture-units.ts
│ ├── maintenance.ts
│ ├── notification-management.ts
│ ├── notifications.ts
│ ├── onboarding.ts
│ ├── repeat-heatmap.ts
│ ├── schedule.ts
│ ├── search.ts
│ ├── seekbar-highlights.ts
│ ├── seekbar-overlay.ts
│ ├── socket.ts
│ ├── splitview.ts
│ ├── start-page.ts
│ ├── stats.ts
│ ├── stream-playlist.ts
│ ├── subtitle-search.ts
│ ├── token-management.ts
│ ├── track-bars.ts
│ ├── transcript.ts
│ ├── user-settings.ts
│ ├── utilities
│ │ ├── ToggleableElement.ts
│ │ ├── date.ts
│ │ ├── fetch-wrappers.ts
│ │ ├── input-interactions.ts
│ │ ├── keycodes.ts
│ │ ├── lectureHallValidator.ts
│ │ ├── paginator.ts
│ │ ├── sliding-window.ts
│ │ ├── smartarray.ts
│ │ ├── storage.ts
│ │ ├── time-utils.ts
│ │ ├── time.ts
│ │ ├── tunnels.ts
│ │ ├── url.ts
│ │ └── ws.ts
│ ├── value-stream.ts
│ ├── video
│ │ └── watchers.ts
│ ├── views
│ │ └── home.ts
│ ├── watch-admin.ts
│ ├── watch.ts
│ └── worker.ts
├── tsconfig.json
├── user.go
├── watch.go
├── web_logger.go
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js
└── worker
├── .gitignore
├── Dockerfile
├── Makefile
├── README.md
├── api
├── api.proto
├── api_server.go
└── api_server_test.go
├── cfg
└── cfg.go
├── client
└── main.go
├── cmd
└── worker
│ └── worker.go
├── edge
├── Dockerfile
├── README.md
├── download.go
├── edge
├── edge.go
├── edge_test.go
├── go.mod
├── go.sum
└── metrics.go
├── entrypoint.sh
├── example.env
├── go.mod
├── go.sum
├── mediamtx.yml
├── pb
├── api.pb.go
└── api_grpc.pb.go
├── rest
├── rest.go
├── selfstream.go
├── selfstream_test.go
└── vod_upload.go
├── rtmp-relay
├── Dockerfile
├── README.md
├── entrypoint.sh
├── main.go
└── rtsp-simple-server.yml
├── tmp
└── .gitkeep
└── worker
├── ffmegtools.go
├── ffmpegtools_test.go
├── notify.go
├── persist.go
├── premiere.go
├── request_handlers.go
├── request_handlers_test.go
├── silence.go
├── status.go
├── stream.go
├── testvid.mp4
├── thumbnails.go
├── transcode.go
├── upload.go
├── upload_test.go
├── vmstat
├── vmstat.go
└── vmstat_test.go
├── waveform.go
└── worker.go
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.go text eol=lf
2 | # Always use LF for all text files
3 | * text=auto eol=lf
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "docker"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 | - package-ecosystem: "github-actions"
8 | directory: "/"
9 | schedule:
10 | interval: "monthly"
11 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ### Motivation and Context
2 |
3 |
4 |
5 | ### Description
6 |
7 |
8 | ### Steps for Testing
9 |
10 | Prerequisites:
11 | - 1 Lecturer
12 | - 2 Students
13 | - 1 Livestream
14 |
15 | 1. Log in
16 | 2. Navigate to a Livestream
17 | 3. ...
18 |
19 | ### Screenshots
20 |
21 |
--------------------------------------------------------------------------------
/.github/workflows/eslint.yml:
--------------------------------------------------------------------------------
1 | name: "Eslint"
2 |
3 | on:
4 | push:
5 | branches: [ dev ]
6 | pull_request:
7 | # The branches below must be a subset of the branches above
8 | branches: [ dev ]
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v5
15 | - name: Install modules
16 | working-directory: ./web
17 | run: npm ci
18 | - name: Run ESLint
19 | working-directory: ./web
20 | run: npx eslint . --ext .ts,.tsx
21 |
--------------------------------------------------------------------------------
/.github/workflows/go-test.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches: [ dev ]
4 | pull_request:
5 | branches: [ dev ]
6 | name: go test
7 | jobs:
8 | test:
9 | strategy:
10 | matrix:
11 | go-version: [ 1.21.x ]
12 | os: [ ubuntu-latest ]
13 | runs-on: ${{ matrix.os }}
14 | steps:
15 | - name: Checkout code
16 | uses: actions/checkout@v5
17 | - name: Install modules
18 | working-directory: ./web
19 | run: npm ci
20 | - name: Install Go
21 | uses: actions/setup-go@v5
22 | with:
23 | go-version: ${{ matrix.go-version }}
24 | - name: go test
25 | run: go test ./...
26 |
--------------------------------------------------------------------------------
/.github/workflows/golangci-lint.yml:
--------------------------------------------------------------------------------
1 | name: golangci-lint
2 | on:
3 | push:
4 | branches: [ dev ]
5 | pull_request:
6 | branches: [ dev ]
7 | permissions:
8 | contents: read
9 | jobs:
10 | golangci:
11 | name: lint
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix: { dir: ['./...', './worker', './worker/edge', './runner'] }
15 | steps:
16 | - uses: actions/setup-go@v5
17 | with:
18 | go-version: 1.22
19 | - uses: actions/checkout@v5
20 | - name: Install modules
21 | working-directory: ./web
22 | run: npm ci
23 | - name: golangci-lint
24 | uses: golangci/golangci-lint-action@v8
25 | with:
26 | version: latest
27 | only-new-issues: true
28 |
29 |
--------------------------------------------------------------------------------
/.github/workflows/pr-opened-notification.yml:
--------------------------------------------------------------------------------
1 | on:
2 | pull_request:
3 | types:
4 | - opened
5 | - reopened
6 | - ready_for_review
7 |
8 | jobs:
9 | send-message:
10 | runs-on: ubuntu-latest
11 | name: Send message via Matrix
12 | if: ${{ !github.event.pull_request.draft && !(startsWith(github.event.sender.login, 'dependabot') || startsWith(github.event.sender.login, 'renovate')) }}
13 | steps:
14 | - name: Send message to test channel
15 | id: matrix-chat-message
16 | uses: fadenb/matrix-chat-message@v0.0.6
17 | with:
18 | homeserver: 'matrix.org'
19 | token: ${{ secrets.MATRIX_TOKEN }}
20 | channel: '!AmOaMEEQgUOTWHlhnA:in.tum.de'
21 | message: |
22 | 🫳🎁 Pull request ready for review by ${{ github.event.sender.login }}:
23 |
24 | ## ${{ github.event.pull_request.title }}
25 |
26 | Please leave your review here: https://github.com/TUM-Dev/gocast/pull/${{ github.event.number }}
27 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /inspectionProfiles/
3 | /shelf/
4 | /workspace.xml
5 | # Datasource local storage ignored files
6 | /dataSources/
7 | /dataSources.local.xml
8 | # Editor-based HTTP Client requests
9 | /httpRequests/
10 | # GitHub Copilot persisted chat sessions
11 | /copilot/chatSessions
12 |
--------------------------------------------------------------------------------
/.idea/TUM-Live-Backend.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/dataSources.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | mysql
6 | true
7 | com.mysql.jdbc.Driver
8 | jdbc:mysql://localhost:3306/tumlive
9 | $ProjectFileDir$
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/jsLibraryMappings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/jsLinters/eslint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/prettier.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/protoeditor.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/TUM_Live__frontend__worker.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/build_dev.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/build_production.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/clean.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/lint_fix.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/postinstall.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/run_dev.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/run_production.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/run_tumlive_go.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/tailwind_compile.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/sqldialects.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/webResources.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: git://github.com/Bahjat/pre-commit-golang
3 | rev: c3086eea8af86847dbdff2e46b85a5fe3c9d9656
4 | hooks:
5 | - id: go-fmt-import
6 | - id: go-vet
7 | - id: go-static-check # install https://staticcheck.io/docs/
8 |
--------------------------------------------------------------------------------
/CNAME:
--------------------------------------------------------------------------------
1 | docs.live.rbg.tum.de
2 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:24 AS node
2 |
3 | WORKDIR /app
4 | COPY web web
5 |
6 | ## remove generated files in case the developer build with npm before
7 | RUN rm -rf web/assets/ts-dist &&\
8 | rm -rf web/assets/css-dist
9 |
10 | WORKDIR /app/web
11 | RUN npm i --no-dev
12 |
13 | FROM golang:1.25.1-alpine3.21 AS build-env
14 |
15 | RUN mkdir /gostuff
16 | WORKDIR /gostuff
17 | COPY go.mod go.sum ./
18 |
19 | # Get dependencies - will also be cached if we won't change mod/sum
20 | RUN go mod download
21 |
22 | WORKDIR /go/src/app
23 | COPY . .
24 | COPY --from=node /app/web/assets ./web/assets
25 | COPY --from=node /app/web/node_modules ./web/node_modules
26 |
27 | # bundle version into binary if specified in build-args, dev otherwise.
28 | ARG version=dev
29 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-w -extldflags '-static' -X main.VersionTag=${version}" -o /go/bin/tumlive cmd/tumlive/tumlive.go
30 |
31 | FROM alpine:3.22
32 | RUN apk add --no-cache tzdata openssl
33 | WORKDIR /app
34 | COPY --from=build-env /go/bin/tumlive .
35 | CMD ["sh", "-c", "sleep 3 && ./tumlive"]
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Joscha Henningsen
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 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: all
2 | all: npm_dependencies go_dependencies bundle
3 |
4 | VERSION := $(shell git rev-parse --short origin/HEAD)
5 |
6 | .PHONY: npm_dependencies
7 | npm_dependencies:
8 | cd web; \
9 | npm i --no-dev
10 |
11 | .PHONY: go_dependencies
12 | go_dependencies:
13 | go get ./...
14 |
15 | .PHONY: bundle
16 | bundle:
17 | go build -o main -ldflags="-X 'main.VersionTag=$(VERSION)'" cmd/tumlive/tumlive.go
18 |
19 | .PHONY: clean
20 | clean:
21 | rm -fr web/node_modules
22 |
23 | .PHONY: install
24 | install:
25 | mv main /bin/tum-live
26 |
27 | .PHONY: mocks
28 | mocks:
29 | go generate ./...
30 |
31 | .PHONY: run_web
32 | run_web:
33 | cd web; \
34 | npm i --include=dev
35 |
36 | .PHONY: run
37 | run:
38 | go run cmd/tumlive/tumlive.go
39 |
40 | .PHONY: test
41 | test:
42 | go test -race ./...
43 |
44 | .PHONY: lint
45 | lint:
46 | golangci-lint run
47 |
48 | .PHONY: protoVoice
49 | protoVoice:
50 | cd voice-service; \
51 | protoc ./subtitles.proto --go-grpc_out=../. --go_out=../.
52 |
53 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Reporting a Vulnerability
4 |
5 | If you found security issues, please send a mail to live-at- rbg.tum.de :)
6 |
--------------------------------------------------------------------------------
/api/api_logger.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "log/slog"
5 | "os"
6 | )
7 |
8 | var logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
9 | Level: slog.LevelDebug,
10 | })).With("service", "api")
11 |
--------------------------------------------------------------------------------
/api/audit_test.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "net/http"
5 | "testing"
6 |
7 | "github.com/gin-gonic/gin"
8 | "github.com/matthiasreumann/gomino"
9 | "go.uber.org/mock/gomock"
10 |
11 | "github.com/TUM-Dev/gocast/dao"
12 | "github.com/TUM-Dev/gocast/mock_dao"
13 | "github.com/TUM-Dev/gocast/model"
14 | "github.com/TUM-Dev/gocast/tools"
15 | "github.com/TUM-Dev/gocast/tools/testutils"
16 | )
17 |
18 | func TestGetAudits(t *testing.T) {
19 | gin.SetMode(gin.TestMode)
20 |
21 | // audit mock
22 | mock := mock_dao.NewMockAuditDao(gomock.NewController(t))
23 | mock.EXPECT().Find(gomock.Any(), gomock.Any(), gomock.Any()).Return([]model.Audit{}, nil).AnyTimes()
24 | mock.EXPECT().Create(gomock.Any()).Return(nil).AnyTimes()
25 |
26 | gomino.TestCases{
27 | "get audits": {
28 | Router: func(r *gin.Engine) {
29 | wrapper := dao.DaoWrapper{AuditDao: mock}
30 | configAuditRouter(r, wrapper)
31 | },
32 | Method: http.MethodGet,
33 | Url: "/api/audits?limit=1&offset=0&types[]=1",
34 | Middlewares: testutils.GetMiddlewares(tools.ErrorHandler, testutils.TUMLiveContext(testutils.TUMLiveContextAdmin)),
35 | ExpectedCode: http.StatusOK,
36 | },
37 | }.Run(t, testutils.Equal)
38 | }
39 |
--------------------------------------------------------------------------------
/api/realtime.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/TUM-Dev/gocast/dao"
5 | "github.com/TUM-Dev/gocast/tools/realtime"
6 | "github.com/TUM-Dev/gocast/tools/realtime/connector"
7 | "github.com/gin-gonic/gin"
8 | )
9 |
10 | type realtimeRoutes struct {
11 | dao.DaoWrapper
12 | }
13 |
14 | var RealtimeInstance = realtime.New(connector.NewMelodyConnector())
15 |
16 | func configGinRealtimeRouter(router *gin.RouterGroup, daoWrapper dao.DaoWrapper) {
17 | routes := realtimeRoutes{daoWrapper}
18 | router.GET("/ws", routes.handleRealtimeConnect)
19 | }
20 |
21 | func (r realtimeRoutes) handleRealtimeConnect(c *gin.Context) {
22 | properties := make(map[string]interface{}, 1)
23 | properties["ctx"] = c
24 | properties["dao"] = r.DaoWrapper
25 |
26 | if err := RealtimeInstance.HandleRequest(c.Writer, c.Request, properties); err != nil {
27 | logger.Warn("Something went wrong while handling Realtime-Socket request", "err", err)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/api/semesters.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "context"
5 | "net/http"
6 |
7 | "github.com/TUM-Dev/gocast/dao"
8 | "github.com/TUM-Dev/gocast/tools/tum"
9 | "github.com/gin-gonic/gin"
10 | )
11 |
12 | func configSemestersRouter(router *gin.Engine, daoWrapper dao.DaoWrapper) {
13 | routes := semesterRoutes{daoWrapper}
14 | router.GET("/api/semesters", routes.getSemesters)
15 | }
16 |
17 | type semesterRoutes struct {
18 | dao.DaoWrapper
19 | }
20 |
21 | func (s semesterRoutes) getSemesters(c *gin.Context) {
22 | includeTestSemester := c.Query("includeTestSemester")
23 |
24 | semesters := s.GetAvailableSemesters(context.Background(), includeTestSemester == "1")
25 | year, term := tum.GetCurrentSemester()
26 | c.JSON(http.StatusOK, gin.H{
27 | "Current": gin.H{
28 | "Year": year,
29 | "TeachingTerm": term,
30 | },
31 | "Semesters": semesters,
32 | })
33 | }
34 |
--------------------------------------------------------------------------------
/api/template/ical.gotemplate:
--------------------------------------------------------------------------------
1 | BEGIN:VCALENDAR
2 | PRODID:Homemade Garbage ICAL generator
3 | VERSION:2.0
4 | BEGIN:VTIMEZONE
5 | TZID:W. Europe Standard Time
6 | BEGIN:STANDARD
7 | DTSTART:16010101T030000
8 | TZOFFSETFROM:+0200
9 | TZOFFSETTO:+0100
10 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10
11 | END:STANDARD
12 | BEGIN:DAYLIGHT
13 | DTSTART:16010101T020000
14 | TZOFFSETFROM:+0100
15 | TZOFFSETTO:+0200
16 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3
17 | END:DAYLIGHT
18 | END:VTIMEZONE
19 | {{range $event := .}}BEGIN:VEVENT
20 | UID:sdfg9438wpwoskegt{{$event.StreamID}}
21 | DTSTART;TZID=W. Europe Standard Time:{{$event.IsoStart}}
22 | DTEND;TZID=W. Europe Standard Time:{{$event.IsoEnd}}
23 | DTSTAMP:{{$event.IsoCreated}}
24 | LOCATION:{{if $event.LectureHallName}}{{$event.LectureHallName}}{{else}}Selfstream{{end}}
25 | SUMMARY:{{$event.CourseName}}
26 | DESCRIPTION:{{$event.StreamID}}
27 | END:VEVENT
28 | {{end}}END:VCALENDAR
29 |
--------------------------------------------------------------------------------
/api/template/ics.gotemplate:
--------------------------------------------------------------------------------
1 | BEGIN:VCALENDAR
2 | METHOD:PUBLISH
3 | VERSION:2.0
4 | CALSCALE:GREGORIAN
5 | PRODID:-//Technische Universität München//DE
6 | BEGIN:VTIMEZONE
7 | TZID:W. Europe Standard Time
8 | BEGIN:STANDARD
9 | DTSTART:16010101T030000
10 | TZOFFSETFROM:+0200
11 | TZOFFSETTO:+0100
12 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10
13 | END:STANDARD
14 | BEGIN:DAYLIGHT
15 | DTSTART:16010101T020000
16 | TZOFFSETFROM:+0100
17 | TZOFFSETTO:+0200
18 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3
19 | END:DAYLIGHT
20 | END:VTIMEZONE
21 | {{range $entry := .}}BEGIN:VEVENT
22 | UID:{{$entry.ID}}@live.rbg.tum.de
23 | DTSTART;TZID=W. Europe Standard Time:{{$entry.Start}}
24 | DTEND;TZID=W. Europe Standard Time:{{$entry.End}}
25 | DTSTAMP:{{$entry.CreatedAt}}
26 | URL:{{$entry.Url}}
27 | LOCATION:{{$entry.Location}}
28 | SUMMARY:{{$entry.Summary}}
29 | DESCRIPTION:{{$entry.Description}}\n{{$entry.Url}}\n{{$entry.Location}}
30 | END:VEVENT
31 | {{end}}END:VCALENDAR
32 |
--------------------------------------------------------------------------------
/api/worker.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/TUM-Dev/gocast/dao"
7 | "github.com/TUM-Dev/gocast/tools"
8 | "github.com/gin-gonic/gin"
9 | )
10 |
11 | func configWorkerRouter(r *gin.Engine, daoWrapper dao.DaoWrapper) {
12 | g := r.Group("/api/workers")
13 | g.Use(tools.Admin)
14 |
15 | routes := workerRoutes{dao: daoWrapper.WorkerDao}
16 |
17 | g.DELETE("/:id", routes.deleteWorker)
18 | }
19 |
20 | type workerRoutes struct {
21 | dao dao.WorkerDao
22 | }
23 |
24 | func (r workerRoutes) deleteWorker(c *gin.Context) {
25 | id := c.Param("id")
26 | err := r.dao.DeleteWorker(id)
27 | if err != nil {
28 | logger.Error("can not delete worker", "err", err)
29 | _ = c.Error(tools.RequestError{
30 | Status: http.StatusInternalServerError,
31 | CustomMessage: "can not delete worker",
32 | Err: err,
33 | })
34 | return
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/apiv2/buf.gen.yaml:
--------------------------------------------------------------------------------
1 | version: v1
2 | plugins:
3 | - name: go
4 | out: protobuf
5 | opt:
6 | - paths=source_relative
7 | - name: go-grpc
8 | out: protobuf
9 | opt:
10 | - paths=source_relative
11 | - name: grpc-gateway
12 | out: protobuf
13 | opt:
14 | - paths=source_relative
15 | - allow_repeated_fields_in_body=true
16 | - name: openapiv2
17 | out: docs
18 | opt:
19 | - allow_repeated_fields_in_body=true
20 |
--------------------------------------------------------------------------------
/apiv2/buf.lock:
--------------------------------------------------------------------------------
1 | # Generated by buf. DO NOT EDIT.
2 | version: v1
3 |
--------------------------------------------------------------------------------
/apiv2/buf.yaml:
--------------------------------------------------------------------------------
1 | version: v1
2 | deps: []
3 | lint:
4 | use:
5 | - DEFAULT
6 | breaking:
7 | use:
8 | - FILE
--------------------------------------------------------------------------------
/apiv2/generate.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # needs buf: https://docs.buf.build/installation#github-releases
4 | BASEDIR=$(dirname "$0")
5 | echo making sure that this script is run from $BASEDIR
6 | pushd $BASEDIR > /dev/null
7 |
8 | echo updating the generated files
9 | export PATH="$PATH:$(go env GOPATH)/bin"
10 | buf dep update || exit 1
11 | buf generate || exit 1
12 |
13 | echo making sure that all artifacts we don\'t need are cleaned up
14 | rm -f google/api/*.go
15 | rm -f google/api/*.swagger.json
16 |
17 | echo moving the generated docs to the server directory
18 | mv ./docs/server/apiv2.swagger.json ./server/docs || exit 1
19 |
20 | echo making sure that all artifacts we don\'t need are cleaned up
21 | rm -rf docs docs/google docs/protoc-gen-openapiv2 protobuf/google protobuf/protoc-gen-openapiv2
22 |
23 | echo making sure that the generated files are formatted
24 | go fmt server/*.go || exit 1
25 | goimports -w server/*.go || exit 1
26 | buf format -w --path server || exit 1
27 |
28 | # clean up the stack
29 | popd > /dev/null
30 |
--------------------------------------------------------------------------------
/apiv2/google/api/annotations.proto:
--------------------------------------------------------------------------------
1 | // Copyright 2015 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | syntax = "proto3";
16 |
17 | package google.api;
18 |
19 | import "google/api/http.proto";
20 | import "google/protobuf/descriptor.proto";
21 |
22 | option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
23 | option java_multiple_files = true;
24 | option java_outer_classname = "AnnotationsProto";
25 | option java_package = "com.google.api";
26 | option objc_class_prefix = "GAPI";
27 |
28 | extend google.protobuf.MethodOptions {
29 | // See `HttpRule`.
30 | HttpRule http = 72295728;
31 | }
32 |
--------------------------------------------------------------------------------
/apiv2/installBuf.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | BASEDIR=$(dirname "$0")
4 | echo making sure that this script is run from $BASEDIR
5 | pushd $BASEDIR > /dev/null
6 |
7 | echo downloading...
8 | go get github.com/bufbuild/buf/cmd/buf \
9 | github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \
10 | github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \
11 | google.golang.org/protobuf/cmd/protoc-gen-go \
12 | google.golang.org/grpc/cmd/protoc-gen-go-grpc
13 |
14 | echo installing...
15 | go install \
16 | github.com/bufbuild/buf/cmd/buf \
17 | github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \
18 | github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \
19 | google.golang.org/protobuf/cmd/protoc-gen-go \
20 | google.golang.org/grpc/cmd/protoc-gen-go-grpc
21 |
22 |
23 | echo tidiing up
24 | go mod tidy
25 |
26 | popd
27 |
--------------------------------------------------------------------------------
/apiv2/server/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Swagger UI
7 |
8 |
9 |
10 |
11 |
12 |
13 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/apiv2/server/semester.go:
--------------------------------------------------------------------------------
1 | package apiv2
2 |
3 | import (
4 | "context"
5 |
6 | h "github.com/TUM-Dev/gocast/apiv2/helpers"
7 | protobuf "github.com/TUM-Dev/gocast/apiv2/protobuf/server"
8 | "github.com/TUM-Dev/gocast/tools/tum"
9 | "google.golang.org/protobuf/types/known/emptypb"
10 | )
11 |
12 | // GetSemesters retrieves all available semesters and the current semester.
13 | func (a *API) GetSemesters(ctx context.Context, req *emptypb.Empty) (*protobuf.GetSemestersResponse, error) {
14 | a.log.Info("GetSemesters")
15 |
16 | semesters := a.dao.GetAvailableSemesters(ctx, false)
17 | year, term := tum.GetCurrentSemester()
18 |
19 | resp := &protobuf.GetSemestersResponse{
20 | Current: &protobuf.Semester{
21 | Year: uint32(year),
22 | TeachingTerm: term,
23 | },
24 | Semesters: make([]*protobuf.Semester, len(semesters)),
25 | }
26 |
27 | for i, semester := range semesters {
28 | resp.Semesters[i] = h.ParseSemesterToProto(semester)
29 | }
30 |
31 | return resp, nil
32 | }
33 |
--------------------------------------------------------------------------------
/branding/branding.yaml:
--------------------------------------------------------------------------------
1 | title: MUT-Live
2 | description: MUT-Live, non-existing live streaming, and VoD service
--------------------------------------------------------------------------------
/branding/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/branding/favicon.ico
--------------------------------------------------------------------------------
/branding/icons-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/branding/icons-192.png
--------------------------------------------------------------------------------
/branding/icons-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/branding/icons-512.png
--------------------------------------------------------------------------------
/branding/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/branding/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "MUT-Live",
3 | "short_name": "MUT-Live",
4 | "icons": [
5 | {
6 | "src": "/static/assets/img/icons-192.png",
7 | "type": "image/png",
8 | "sizes": "192x192"
9 | },
10 | {
11 | "src": "/static/assets/img/icons-512.png",
12 | "type": "image/png",
13 | "sizes": "512x512"
14 | }
15 | ],
16 | "start_url": "/?source=pwa",
17 | "background_color": "#161c22",
18 | "display_override": ["window-control-overlay", "minimal-ui"],
19 | "display": "standalone",
20 | "scope": "/",
21 | "theme_color": "#161c22",
22 | "description": "MUT-Live, non-existing live streaming, and VoD service",
23 | "screenshots": []
24 | }
--------------------------------------------------------------------------------
/branding/thumb-fallback.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/branding/thumb-fallback.png
--------------------------------------------------------------------------------
/cmd/modelGen/daoTemplate.tmpl:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | import (
4 | "context"
5 | "github.com/TUM-Dev/gocast/model"
6 | "gorm.io/gorm"
7 | )
8 |
9 | //go:generate go tool mockgen -source={{.NamePrivate}}.go -destination ../mock_dao/{{.NamePrivate}}.go
10 |
11 | type {{.NameExported}}Dao interface {
12 | // Get {{.NameExported}} by ID
13 | Get(context.Context, uint) (model.{{.NameExported}}, error)
14 |
15 | // Create a new {{.NameExported}} for the database
16 | Create(context.Context, *model.{{.NameExported}}) error
17 |
18 | // Delete a {{.NameExported}} by id.
19 | Delete(context.Context, uint) error
20 | }
21 |
22 | type {{.NamePrivate}}Dao struct {
23 | db *gorm.DB
24 | }
25 |
26 | func New{{.NameExported}}Dao() {{.NameExported}}Dao {
27 | return {{.NamePrivate}}Dao{db: DB}
28 | }
29 |
30 | // Get a {{.NameExported}} by id.
31 | func (d {{.NamePrivate}}Dao) Get(c context.Context, id uint) (res model.{{.NameExported}}, err error) {
32 | return res, d.db.WithContext(c).First(&res, id).Error
33 | }
34 |
35 | // Create a {{.NameExported}}.
36 | func (d {{.NamePrivate}}Dao) Create(c context.Context, it *model.{{.NameExported}}) error {
37 | return d.db.WithContext(c).Create(it).Error
38 | }
39 |
40 | // Delete a {{.NameExported}} by id.
41 | func (d {{.NamePrivate}}Dao) Delete(c context.Context, id uint) error {
42 | return d.db.WithContext(c).Delete(&model.{{.NameExported}}{}, id).Error
43 | }
44 |
--------------------------------------------------------------------------------
/cmd/modelGen/modelTemplate.tmpl:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "gorm.io/gorm"
4 |
5 | // {{.NameExported}} represents todo...
6 | type {{.NameExported}} struct {
7 | gorm.Model
8 |
9 | // todo. Please specify column, type and not null (if required):
10 | // Name string `gorm:"column:name;type:text;not null;default:'unnamed'"`
11 | }
12 |
13 | // TableName returns the name of the table for the {{.NameExported}} model in the database.
14 | func (*{{.NameExported}}) TableName() string {
15 | return "{{.NamePrivate}}" // todo
16 | }
17 |
18 | // BeforeCreate todo
19 | func ({{.NameReceiver}} *{{.NameExported}}) BeforeCreate(tx *gorm.DB) (err error) {
20 | return nil
21 | }
22 |
23 | // AfterFind todo
24 | func ({{.NameReceiver}} *{{.NameExported}}) AfterFind(tx *gorm.DB) (err error) {
25 | return nil
26 | }
27 |
--------------------------------------------------------------------------------
/dao/audit.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | import (
4 | "github.com/TUM-Dev/gocast/model"
5 | "gorm.io/gorm"
6 | )
7 |
8 | //go:generate go tool mockgen -source=audit.go -destination ../mock_dao/audit.go
9 |
10 | type AuditDao interface {
11 | // Create a new audit for the database
12 | Create(*model.Audit) error
13 | // Find audits
14 | Find(limit int, offset int, types ...model.AuditType) (audits []model.Audit, err error)
15 | }
16 |
17 | type auditDao struct {
18 | db *gorm.DB
19 | }
20 |
21 | func (a auditDao) Find(limit int, offset int, types ...model.AuditType) (audits []model.Audit, err error) {
22 | return audits, a.db.
23 | Preload("User").
24 | Model(&model.Audit{}).
25 | Where("type in ?", types).
26 | Order("created_at desc").
27 | Limit(limit).
28 | Offset(offset).
29 | Find(&audits).Error
30 | }
31 |
32 | func (a auditDao) Create(audit *model.Audit) error {
33 | return a.db.Create(audit).Error
34 | }
35 |
36 | func NewAuditDao() AuditDao {
37 | return auditDao{db: DB}
38 | }
39 |
--------------------------------------------------------------------------------
/dao/bookmark.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | import (
4 | "github.com/TUM-Dev/gocast/model"
5 | "gorm.io/gorm"
6 | )
7 |
8 | //go:generate go tool mockgen -source=bookmark.go -destination ../mock_dao/bookmark.go
9 |
10 | type BookmarkDao interface {
11 | Add(*model.Bookmark) error
12 | GetByID(uint) (model.Bookmark, error)
13 | GetByStreamID(uint, uint) ([]model.Bookmark, error)
14 | Update(*model.Bookmark) error
15 | Delete(uint) error
16 | }
17 |
18 | type bookmarkDao struct {
19 | db *gorm.DB
20 | }
21 |
22 | func NewBookmarkDao() BookmarkDao {
23 | return bookmarkDao{db: DB}
24 | }
25 |
26 | func (d bookmarkDao) Add(bookmark *model.Bookmark) error {
27 | return d.db.Save(bookmark).Error
28 | }
29 |
30 | func (d bookmarkDao) GetByID(id uint) (bookmark model.Bookmark, err error) {
31 | err = d.db.Where("id = ?", id).First(&bookmark).Error
32 | return bookmark, err
33 | }
34 |
35 | func (d bookmarkDao) GetByStreamID(streamID uint, userID uint) (bookmarks []model.Bookmark, err error) {
36 | err = d.db.Order("hours, minutes, seconds ASC").Where("stream_id = ? AND user_id = ?", streamID, userID).Find(&bookmarks).Error
37 | return bookmarks, err
38 | }
39 |
40 | func (d bookmarkDao) Update(bookmark *model.Bookmark) error {
41 | return d.db.Model(bookmark).Updates(bookmark).Error
42 | }
43 |
44 | func (d bookmarkDao) Delete(id uint) error {
45 | return d.db.Delete(&model.Bookmark{}, "id = ?", id).Error
46 | }
47 |
--------------------------------------------------------------------------------
/dao/cache.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | import "github.com/dgraph-io/ristretto/v2"
4 |
5 | var Cache *ristretto.Cache[string, any]
6 |
--------------------------------------------------------------------------------
/dao/camera-preset.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | import (
4 | "gorm.io/gorm"
5 |
6 | "github.com/TUM-Dev/gocast/model"
7 | )
8 |
9 | //go:generate go tool mockgen -source=camera-preset.go -destination ../mock_dao/camera-preset.go
10 |
11 | type CameraPresetDao interface {
12 | GetDefaultCameraPreset(lectureHallID uint) (res model.CameraPreset, err error)
13 | }
14 |
15 | type cameraPresetDao struct {
16 | db *gorm.DB
17 | }
18 |
19 | func NewCameraPresetDao() CameraPresetDao {
20 | return cameraPresetDao{db: DB}
21 | }
22 |
23 | func (d cameraPresetDao) GetDefaultCameraPreset(lectureHallID uint) (model.CameraPreset, error) {
24 | var res model.CameraPreset
25 | return res, DB.First(&res, "lecture_hall_id = ? AND is_default", lectureHallID).Error
26 | }
27 |
--------------------------------------------------------------------------------
/dao/dao_logger.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | import (
4 | "log/slog"
5 | "os"
6 | )
7 |
8 | var logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
9 | Level: slog.LevelDebug,
10 | })).With("service", "dao")
11 |
--------------------------------------------------------------------------------
/dao/info-page.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | import (
4 | "github.com/TUM-Dev/gocast/model"
5 | "gorm.io/gorm"
6 | )
7 |
8 | //go:generate go tool mockgen -source=info-page.go -destination ../mock_dao/info-page.go
9 |
10 | type InfoPageDao interface {
11 | New(*model.InfoPage) error
12 | GetAll() ([]model.InfoPage, error)
13 | GetById(uint) (model.InfoPage, error)
14 | Update(uint, *model.InfoPage) error
15 | }
16 |
17 | type infoPageDao struct {
18 | db *gorm.DB
19 | }
20 |
21 | func NewInfoPageDao() InfoPageDao {
22 | return infoPageDao{db: DB}
23 | }
24 |
25 | func (d infoPageDao) New(page *model.InfoPage) error {
26 | return DB.Create(page).Error
27 | }
28 |
29 | func (d infoPageDao) GetAll() (pages []model.InfoPage, err error) {
30 | err = DB.Find(&pages).Error
31 | return pages, err
32 | }
33 |
34 | func (d infoPageDao) GetById(id uint) (page model.InfoPage, err error) {
35 | err = DB.Find(&page, "id = ?", id).Error
36 | return page, err
37 | }
38 |
39 | func (d infoPageDao) Update(id uint, page *model.InfoPage) error {
40 | return DB.Model(&model.InfoPage{}).Where("id = ?", id).Updates(page).Error
41 | }
42 |
--------------------------------------------------------------------------------
/dao/migrations/202201280.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "github.com/TUM-Dev/gocast/model"
5 | "github.com/go-gormigrate/gormigrate/v2"
6 | "gorm.io/gorm"
7 | )
8 |
9 | // Migrate202201280 Deletes all messages longer than 200 characters and all empty messages
10 | func Migrate202201280() *gormigrate.Migration {
11 | return &gormigrate.Migration{
12 | ID: "202201280",
13 | Migrate: func(tx *gorm.DB) error {
14 | err := tx.Where("LENGTH(message) > 200").Delete(&model.Chat{}).Error // clean up messages from before length limit
15 | if err != nil {
16 | return err
17 | }
18 | return tx.Where("REPLACE(message, ' ', '') = ''").Delete(&model.Chat{}).Error // clean up messages from before empty constraint
19 | },
20 | Rollback: func(tx *gorm.DB) error {
21 | return nil
22 | },
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/dao/migrations/202207240.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "github.com/TUM-Dev/gocast/model"
5 | "github.com/go-gormigrate/gormigrate/v2"
6 | "gorm.io/gorm"
7 | )
8 |
9 | // Migrate202207240 Drops the column "stream_status" from streams, because they are superseded by transcodingProgresses
10 | func Migrate202207240() *gormigrate.Migration {
11 | return &gormigrate.Migration{
12 | ID: "202207240",
13 | Migrate: func(tx *gorm.DB) error {
14 | m := tx.Migrator()
15 | if m.HasColumn(&model.Stream{}, "stream_status") {
16 | return m.DropColumn(&model.Stream{}, "stream_status")
17 | }
18 | return nil
19 | },
20 | Rollback: func(tx *gorm.DB) error {
21 | return nil
22 | },
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/dao/migrations/202208100.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "github.com/TUM-Dev/gocast/model"
5 | "github.com/go-gormigrate/gormigrate/v2"
6 | "gorm.io/gorm"
7 | )
8 |
9 | // Migrate202210080 adjusts the user setting type to compensate for removal of 'enable_chromcast'.
10 | func Migrate202210080() *gormigrate.Migration {
11 | return &gormigrate.Migration{
12 | ID: "202210080",
13 | Migrate: func(tx *gorm.DB) error {
14 | // Delete all chrome cast settings.
15 | err := tx.Unscoped().Model(&model.UserSetting{}).Where("type = 3").Delete(&model.UserSetting{}).Error
16 | if err != nil {
17 | return err
18 | }
19 | // CustomPlaybackSpeed previously was of type 4, since type 3 is deleted, it now becomes type 3.
20 | return tx.Model(&model.UserSetting{}).Where("type = 4").Update("type", 3).Error
21 | },
22 | Rollback: func(tx *gorm.DB) error {
23 | return nil
24 | },
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/dao/migrations/202208110.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "github.com/TUM-Dev/gocast/model"
5 | "github.com/go-gormigrate/gormigrate/v2"
6 | "gorm.io/gorm"
7 | )
8 |
9 | // Migrate202208110 Drops the column "live_enabled" from courses, since it isn't needed anymore
10 | func Migrate202208110() *gormigrate.Migration {
11 | return &gormigrate.Migration{
12 | ID: "202208110",
13 | Migrate: func(tx *gorm.DB) error {
14 | m := tx.Migrator()
15 | if m.HasColumn(&model.Course{}, "live_enabled") {
16 | return m.DropColumn(&model.Course{}, "live_enabled")
17 | }
18 | return nil
19 | },
20 | Rollback: func(tx *gorm.DB) error {
21 | return nil
22 | },
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/dao/migrations/202210270.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "github.com/TUM-Dev/gocast/model"
5 | "github.com/go-gormigrate/gormigrate/v2"
6 | "gorm.io/gorm"
7 | )
8 |
9 | // Migrate202210270 Drops the column "paused" from streams.
10 | func Migrate202210270() *gormigrate.Migration {
11 | return &gormigrate.Migration{
12 | ID: "202210270",
13 | Migrate: func(tx *gorm.DB) error {
14 | m := tx.Migrator()
15 | if m.HasColumn(&model.Stream{}, "paused") {
16 | return m.DropColumn(&model.Stream{}, "paused")
17 | }
18 | return nil
19 | },
20 | Rollback: func(tx *gorm.DB) error {
21 | return nil
22 | },
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/dao/migrations/202212010.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "github.com/TUM-Dev/gocast/model"
5 | "github.com/go-gormigrate/gormigrate/v2"
6 | "gorm.io/gorm"
7 | )
8 |
9 | // Migrate202212010 changes data type of the 'name' column in the 'user' table from 'longtext' to 'varchar(80)'
10 | func Migrate202212010() *gormigrate.Migration {
11 | return &gormigrate.Migration{
12 | ID: "202212010",
13 | Migrate: func(tx *gorm.DB) error {
14 | err := tx.Set(`gorm:"type:varchar(80); not null" json:"name"`, "ENGINE=InnoDB").AutoMigrate(&model.User{})
15 | if err != nil {
16 | return err
17 | }
18 | return nil
19 | },
20 | Rollback: func(tx *gorm.DB) error {
21 | return nil
22 | },
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/dao/migrations/202212020.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "github.com/go-gormigrate/gormigrate/v2"
5 | "gorm.io/gorm"
6 | )
7 |
8 | // Migrate202212020 Drops the table "prefetched_courses".
9 | func Migrate202212020() *gormigrate.Migration {
10 | return &gormigrate.Migration{
11 | ID: "202212020",
12 | Migrate: func(tx *gorm.DB) error {
13 | m := tx.Migrator()
14 | if m.HasTable("prefetched_courses") {
15 | return m.DropTable("prefetched_courses")
16 | }
17 | return nil
18 | },
19 | Rollback: func(tx *gorm.DB) error {
20 | return nil
21 | },
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/dao/migrations/202301006.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "github.com/go-gormigrate/gormigrate/v2"
5 | "gorm.io/gorm"
6 | )
7 |
8 | // Migrate202301006 Drops the table "chat_user_likes".
9 | func Migrate202301006() *gormigrate.Migration {
10 | return &gormigrate.Migration{
11 | ID: "202301006",
12 | Migrate: func(tx *gorm.DB) error {
13 | m := tx.Migrator()
14 | if m.HasTable("chat_user_likes") {
15 | return m.DropTable("chat_user_likes")
16 | }
17 | return nil
18 | },
19 | Rollback: func(tx *gorm.DB) error {
20 | return nil
21 | },
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/dao/transcoding-failure.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | import (
4 | "github.com/TUM-Dev/gocast/model"
5 | "gorm.io/gorm"
6 | )
7 |
8 | //go:generate go tool mockgen -source=transcoding-failure.go -destination ../mock_dao/transcoding-failure.go
9 |
10 | type TranscodingFailureDao interface {
11 | // All returns all open transcoding failures
12 | All() ([]model.TranscodingFailure, error)
13 |
14 | // New creates a new transcoding failure
15 | New(*model.TranscodingFailure) error
16 |
17 | // Delete deletes a transcoding failure
18 | Delete(id uint) error
19 | }
20 |
21 | func NewTranscodingFailureDao() TranscodingFailureDao {
22 | return &transcodingFailureDao{db: DB}
23 | }
24 |
25 | type transcodingFailureDao struct {
26 | db *gorm.DB
27 | }
28 |
29 | // All returns all open transcoding failures
30 | func (t transcodingFailureDao) All() (failures []model.TranscodingFailure, err error) {
31 | return failures, DB.Preload("Stream").Find(&failures).Error
32 | }
33 |
34 | // New creates a new transcoding failure
35 | func (t transcodingFailureDao) New(failure *model.TranscodingFailure) error {
36 | return DB.Create(failure).Error
37 | }
38 |
39 | // Delete deletes a transcoding failure
40 | func (t transcodingFailureDao) Delete(id uint) error {
41 | return DB.Delete(&model.TranscodingFailure{}, id).Error
42 | }
43 |
--------------------------------------------------------------------------------
/dao/upload_key.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | import (
4 | "github.com/TUM-Dev/gocast/model"
5 | "gorm.io/gorm"
6 | )
7 |
8 | //go:generate go tool mockgen -source=upload_key.go -destination ../mock_dao/upload_key.go
9 |
10 | type UploadKeyDao interface {
11 | GetUploadKey(key string) (model.UploadKey, error)
12 | CreateUploadKey(key string, stream uint, videoType model.VideoType) error
13 | DeleteUploadKey(key model.UploadKey) error
14 | }
15 |
16 | type uploadKeyDao struct {
17 | db *gorm.DB
18 | }
19 |
20 | func (u uploadKeyDao) GetUploadKey(key string) (k model.UploadKey, err error) {
21 | return k, u.db.Preload("Stream").First(&k, "upload_key = ?", key).Error
22 | }
23 |
24 | func (u uploadKeyDao) CreateUploadKey(key string, stream uint, videoType model.VideoType) error {
25 | return u.db.Create(&model.UploadKey{UploadKey: key, StreamID: stream, VideoType: videoType}).Error
26 | }
27 |
28 | func (u uploadKeyDao) DeleteUploadKey(key model.UploadKey) error {
29 | return u.db.Unscoped().Delete(&key).Error
30 | }
31 |
32 | func NewUploadKeyDao() UploadKeyDao {
33 | return &uploadKeyDao{db: DB}
34 | }
35 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | site
2 |
--------------------------------------------------------------------------------
/docs/docs/chat-img/activate.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/docs/chat-img/activate.jpg
--------------------------------------------------------------------------------
/docs/docs/chat-img/anonymous.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/docs/chat-img/anonymous.jpg
--------------------------------------------------------------------------------
/docs/docs/chat-img/approve.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/docs/chat-img/approve.jpg
--------------------------------------------------------------------------------
/docs/docs/chat-img/at-ing.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/docs/chat-img/at-ing.jpg
--------------------------------------------------------------------------------
/docs/docs/chat-img/dismiss.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/docs/chat-img/dismiss.jpg
--------------------------------------------------------------------------------
/docs/docs/chat-img/emojis.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/docs/chat-img/emojis.jpg
--------------------------------------------------------------------------------
/docs/docs/chat-img/poll-result.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/docs/chat-img/poll-result.jpg
--------------------------------------------------------------------------------
/docs/docs/chat-img/polls.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/docs/chat-img/polls.jpg
--------------------------------------------------------------------------------
/docs/docs/chat-img/resolve.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/docs/chat-img/resolve.jpg
--------------------------------------------------------------------------------
/docs/docs/chat-img/resolved-with-mark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/docs/chat-img/resolved-with-mark.jpg
--------------------------------------------------------------------------------
/docs/docs/course-nav.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/docs/course-nav.png
--------------------------------------------------------------------------------
/docs/docs/frontend-components.md:
--------------------------------------------------------------------------------
1 | # Frontend Components
2 |
3 | This file contains a collection of html snippets for reuse purposes until
4 | we implement 'real' components.
5 |
6 | ## Buttons
7 |
8 | ### Utility Button
9 |
10 | Currently in use on the admin course page for stream sorting.
11 |
12 | ```
13 |
19 | ```
--------------------------------------------------------------------------------
/docs/docs/index.md:
--------------------------------------------------------------------------------
1 | # TUM-Live Documentation
2 |
3 | Contents of this documentation:
4 |
5 | - [User Guide](user-guide/)
6 | - [Your TUM-Live Admin account](user-guide/#your-tum-live-admin-account)
7 | - [Create a course](user-guide/#create-a-course)
8 | - [Manage Lectures](user-guide/#manage-lectures)
--------------------------------------------------------------------------------
/docs/docs/lecture-edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/docs/lecture-edit.png
--------------------------------------------------------------------------------
/docs/docs/new-course-prompt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/docs/new-course-prompt.png
--------------------------------------------------------------------------------
/docs/docs/new-course-prompt.png.kra:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/docs/new-course-prompt.png.kra
--------------------------------------------------------------------------------
/docs/docs/new-course.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/docs/new-course.png
--------------------------------------------------------------------------------
/docs/docs/video-img/sections-on-watch-page.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/docs/video-img/sections-on-watch-page.jpg
--------------------------------------------------------------------------------
/docs/docs/video-img/video-sections.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/docs/video-img/video-sections.jpg
--------------------------------------------------------------------------------
/docs/docs/video.md:
--------------------------------------------------------------------------------
1 | # Video
2 |
3 | # VOD Sections
4 |
5 | Structuring lectures into sections makes lectures more rewatchable.
6 | A click on a section will jump to the given timestamp. Hence, students can repeat
7 | lectures section-wise.
8 |
9 | 
10 |
11 | ## Create Sections
12 |
13 | On the Admin page's sidebar navigate to:
14 |
15 | `Courses > 'Term' > 'Your Course' - Settings`
16 |
17 | There you will find a list of lectures. Sections can only be added
18 | to VOD streams. (Visualized by the Green VOD Symbol)
19 |
20 | The UI for managing video sections is very intuitive.
21 |
22 | 
--------------------------------------------------------------------------------
/docs/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: TUM-Live
2 | site_url: https://docs.live.mm.rbg.tum.de/
3 | theme:
4 | name: readthedocs
5 | highlightjs: true
6 | hljs_languages:
7 | - yaml
8 | - rust
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | mkdocs
--------------------------------------------------------------------------------
/docs/styling/img/dark_form.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/styling/img/dark_form.jpg
--------------------------------------------------------------------------------
/docs/styling/img/dark_input.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/styling/img/dark_input.jpg
--------------------------------------------------------------------------------
/docs/styling/img/dark_primary_button.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/styling/img/dark_primary_button.jpg
--------------------------------------------------------------------------------
/docs/styling/img/dark_secondary_buttons.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/styling/img/dark_secondary_buttons.jpg
--------------------------------------------------------------------------------
/docs/styling/img/light_form.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/styling/img/light_form.jpg
--------------------------------------------------------------------------------
/docs/styling/img/light_input.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/styling/img/light_input.jpg
--------------------------------------------------------------------------------
/docs/styling/img/light_primary_button.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/styling/img/light_primary_button.jpg
--------------------------------------------------------------------------------
/docs/styling/img/light_secondary_buttons.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs/styling/img/light_secondary_buttons.jpg
--------------------------------------------------------------------------------
/docs_v2/.gitignore:
--------------------------------------------------------------------------------
1 | w# Dependencies
2 | /node_modules
3 |
4 | # Production
5 | /build
6 |
7 | # Generated files
8 | .docusaurus
9 | .cache-loader
10 |
11 | # Misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
22 | .vercel
23 |
--------------------------------------------------------------------------------
/docs_v2/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
3 | };
4 |
--------------------------------------------------------------------------------
/docs_v2/blog/changelogs/v1.2.27.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "V1.2.27"
3 | date: 2023-04-24T20:56:06+02:00
4 | draft: false
5 | summary: "This release contains one small bugfix."
6 | tags: [bugfix]
7 | ---
8 |
9 | This release contains one small bugfix.
10 |
11 | - [fix edge server tests and log import](https://github.com/joschahenningsen/TUM-Live/commit/04aa8ca42e32cd9684ee815e445902125981b5c5) by [@joschahenningsen](https://github.com/joschahenningsen)
12 | - This fixes an issue with the latest deployment where the wrong log package was imported in the Edge submodule.
13 |
--------------------------------------------------------------------------------
/docs_v2/blog/changelogs/v1.4.18.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "V1.4.18"
3 | date: 2024-05-06T20:56:06+02:00
4 | draft: false
5 | summary: "This release contains a few bug fixes and improvements. It also contains some new features and changes."
6 | tags: [bugfix, feature]
7 | ---
8 |
9 | This release contains a few bug fixes and improvements.
10 | It also contains some new features and changes.
11 |
12 | Bugfixes:
13 | - Fixed a [bug](https://github.com/TUM-Dev/gocast/pull/1324) where the chat stops working after a too long message
14 | - Fixed a [bug](https://github.com/TUM-Dev/gocast/pull/1350) where the `watched` state of a VoD wasn't detected correctly
15 | - Fixed a [bug](https://github.com/TUM-Dev/gocast/pull/1340) where private VoDs were still listed publicly
16 | - [Skip silence button](https://github.com/TUM-Dev/gocast/pull/1326) not shown if you seek into the silence
17 |
18 | Features:
19 | - You can now select, that the [beta mode](https://github.com/TUM-Dev/gocast/pull/1328) is your default mode
20 |
--------------------------------------------------------------------------------
/docs_v2/docs/features/LectureHallStreams.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Stream from your Lecture Hall"
3 | sidebar_position: 1
4 | description: "Automatic Broadcasting and Recording of Auditoriums"
5 | ---
6 |
7 | ## Automatic Broadcasting and Recording of Auditoriums
8 |
9 | With GoCast, you can easily stream your lectures to the internet. This allows students to follow the lecture from home
10 | or on the go. If you wish so, a recording of the lecture is also available for later viewing.
11 |
12 | For this purpose, we have installed Streaming Media Processors (SMPs) in many lecture halls at TUM. These devices are
13 | capable of capturing the video and audio of the lecture and sending it to our servers for broadcasting.
14 |
15 | For a guide on how to stream from a lecture hall, please refer to the [Lecture Hall Streaming Guide](/docs/usage/lecturehall-streaming.md).
16 |
17 | ---
18 |
19 | # Self-Streaming using OBS, Zoom or other Software
20 |
21 | You can also stream your lectures yourself with any streaming software you like. We recommend OBS for this purpose.
22 |
23 | For instructions on how to self-stream, please refer to the [Self-Streaming Guide](/docs/usage/self-streaming.md).
24 |
--------------------------------------------------------------------------------
/docs_v2/docs/features/VideoOnDemand.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Video On Demand"
3 | sidebar_position: 2
4 | description: "Upload videos to the server and stream them to the students."
5 | ---
6 |
7 | # Video On Demand
8 |
9 | Video On Demand (VoD) is a feature that allows you to upload videos to the server and stream them to the students. This
10 | feature is useful for hosting videos that you want to share with your students.
11 |
12 | You can also record your live classes and get them automatically uploaded to the server. This way, students who missed
13 | the live class can watch the recording later.
--------------------------------------------------------------------------------
/docs_v2/docs/features/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Features",
3 | "position": 2,
4 | "link": {
5 | "type": "generated-index",
6 | "description": "Learn about the features of GoCast."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/docs_v2/docs/features/_index.md:
--------------------------------------------------------------------------------
1 | awdawd
--------------------------------------------------------------------------------
/docs_v2/docs/selfhosting/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Selfhosting",
3 | "position": 4,
4 | "link": {
5 | "type": "generated-index",
6 | "description": "Selfhost GoCast on your network."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/docs_v2/docs/selfhosting/networking.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Networking"
3 | sidebar_position: 3
4 | ---
5 |
6 |
7 | ## Networking
8 |
9 | The following ports need to be exposed to the public:
10 |
11 | | Server (label) | Port |
12 | |----------------------------------|-----------------|
13 | | GoCast Server (tumlive, traefik) | 80 TCP, 443 TCP |
14 | | Worker (worker) | 1935 TCP |
15 | | Edge (edge) | 80 TCP, 443 TCP |
16 |
17 | Between the individual servers, communication should not be firewalled. Auditorium hardware should also be in the same VLAN.
18 |
--------------------------------------------------------------------------------
/docs_v2/docs/usage/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Usage",
3 | "position": 1,
4 | "link": {
5 | "type": "generated-index",
6 | "description": "Learn how to use GoCast."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/docs_v2/docs/usage/chat-img/activate.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/docs/usage/chat-img/activate.jpg
--------------------------------------------------------------------------------
/docs_v2/docs/usage/chat-img/anonymous.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/docs/usage/chat-img/anonymous.jpg
--------------------------------------------------------------------------------
/docs_v2/docs/usage/chat-img/approve.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/docs/usage/chat-img/approve.jpg
--------------------------------------------------------------------------------
/docs_v2/docs/usage/chat-img/at-ing.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/docs/usage/chat-img/at-ing.jpg
--------------------------------------------------------------------------------
/docs_v2/docs/usage/chat-img/dismiss.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/docs/usage/chat-img/dismiss.jpg
--------------------------------------------------------------------------------
/docs_v2/docs/usage/chat-img/emojis.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/docs/usage/chat-img/emojis.jpg
--------------------------------------------------------------------------------
/docs_v2/docs/usage/chat-img/poll-result.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/docs/usage/chat-img/poll-result.jpg
--------------------------------------------------------------------------------
/docs_v2/docs/usage/chat-img/polls.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/docs/usage/chat-img/polls.jpg
--------------------------------------------------------------------------------
/docs_v2/docs/usage/chat-img/resolve.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/docs/usage/chat-img/resolve.jpg
--------------------------------------------------------------------------------
/docs_v2/docs/usage/chat-img/resolved-with-mark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/docs/usage/chat-img/resolved-with-mark.jpg
--------------------------------------------------------------------------------
/docs_v2/docs/usage/course-img/course-nav.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/docs/usage/course-img/course-nav.png
--------------------------------------------------------------------------------
/docs_v2/docs/usage/course-img/lecture-edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/docs/usage/course-img/lecture-edit.png
--------------------------------------------------------------------------------
/docs_v2/docs/usage/course-img/new-course-prompt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/docs/usage/course-img/new-course-prompt.png
--------------------------------------------------------------------------------
/docs_v2/docs/usage/course-img/new-course.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/docs/usage/course-img/new-course.png
--------------------------------------------------------------------------------
/docs_v2/docs/usage/lecturehall-streaming.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Lecture Hall Streaming"
3 | sidebar_position: 3
4 | description: "Streaming from a lecture hall with installed SMPs."
5 | ---
6 |
7 | # Streaming from a lecture hall with installed SMPs
8 |
9 | This guide contains information on how to stream from a lecture hall at TUM.
10 |
11 | ## How to stream
12 |
13 | To stream your lecture from a SMP equipped lecture hall, follow these steps:
14 | 1. Create your course
15 | 2. Create a lecture
16 | 3. Add a room to the lecture
17 | 4. Set the date and time of the lecture (**Important:** Please also set the end time)
18 |
19 | In most cases, your lecture gets imported from TUMOnline. If you want to stream a lecture that is not in TUMOnline,
20 | you can create a new course and lecture in GoCast.
21 |
22 | ## How to create a livestream
23 |
24 | 1. Select `Livestream` and then press `Continue`.
25 | 
26 | 2. Enter your lecture name and select your lecture hall. You also have to insert your lecture start and end.
27 | After these steps, you can press `Create Lecture`.
28 | 
29 | 3. Your lecture will automatically start at the selected time.
30 |
--------------------------------------------------------------------------------
/docs_v2/docs/usage/video-img/sections-on-watch-page.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/docs/usage/video-img/sections-on-watch-page.jpg
--------------------------------------------------------------------------------
/docs_v2/docs/usage/video-img/video-sections.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/docs/usage/video-img/video-sections.jpg
--------------------------------------------------------------------------------
/docs_v2/docs/usage/video.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Video"
3 | sidebar_position: 5
4 | description: "Useful information for VoDs."
5 | ---
6 |
7 | # Video
8 |
9 | ## VoD Sections
10 |
11 | Structuring lectures into sections makes lectures more rewatchable.
12 | A click on a section will jump to the given timestamp. Hence, students can repeat
13 | lectures section-wise.
14 |
15 | 
16 |
17 | ## Create Sections
18 |
19 | On the Admin page's sidebar, navigate to:
20 |
21 | `Courses > 'Term' > 'Your Course' - Settings`
22 |
23 | There you will find a list of lectures. Sections can only be added
24 | to VoD streams. (Visualized by the Green VoD Symbol)
25 |
26 | The UI for managing video sections is very intuitive.
27 |
28 | 
--------------------------------------------------------------------------------
/docs_v2/sidebars.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Creating a sidebar enables you to:
3 | - create an ordered group of docs
4 | - render a sidebar for each doc of that group
5 | - provide next/previous navigation
6 |
7 | The sidebars can be generated from the filesystem, or explicitly defined here.
8 |
9 | Create as many sidebars as you want.
10 | */
11 |
12 | // @ts-check
13 |
14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
15 | const sidebars = {
16 | // By default, Docusaurus generates a sidebar from the docs folder structure
17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
18 |
19 | // But you can create a sidebar manually
20 | /*
21 | tutorialSidebar: [
22 | {
23 | type: 'category',
24 | label: 'Tutorial',
25 | items: ['hello'],
26 | },
27 | ],
28 | */
29 | };
30 |
31 | module.exports = sidebars;
32 |
--------------------------------------------------------------------------------
/docs_v2/src/components/HomepageFeatures/styles.module.css:
--------------------------------------------------------------------------------
1 | .features {
2 | padding: 2rem 0;
3 | text-align: center;
4 | background: linear-gradient(90deg, rgba(2,0,36,1) 0%, rgb(47, 68, 139) 35%, rgb(80, 162, 255) 100%);
5 | color: white;
6 | }
7 |
8 | .featureSvg {
9 | height: 120px;
10 | width: 120px;
11 | }
12 |
13 | .getStarted {
14 | margin-top: 4rem;
15 | text-align: left;
16 | display: flex;
17 | width: 60%; /* This will apply to larger screens */
18 | flex-direction: row;
19 | justify-content: center;
20 | }
21 |
22 | @media (max-width: 768px) {
23 | .getStarted {
24 | width: 100%; /* This will apply to screens that are 768px wide or smaller */
25 | }
26 | }
27 |
28 | .narrowMargin {
29 | margin: 0rem 1rem;
30 | }
--------------------------------------------------------------------------------
/docs_v2/src/pages/markdown-page.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Markdown page example
3 | ---
4 |
5 | # Markdown page example
6 |
7 | You don't need React to write simple standalone pages.
8 |
--------------------------------------------------------------------------------
/docs_v2/src/prism-include-languages.js:
--------------------------------------------------------------------------------
1 | module.exports = function (Prism) {
2 | require('prismjs/components/prism-ruby')(Prism);
3 | require('prismjs/components/prism-python')(Prism);
4 | require('prismjs/components/prism-bash')(Prism);
5 | require('prismjs/components/prism-java')(Prism);
6 | };
--------------------------------------------------------------------------------
/docs_v2/static/deployment/DeploymentDiagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/static/deployment/DeploymentDiagram.png
--------------------------------------------------------------------------------
/docs_v2/static/deployment/runners.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/static/deployment/runners.png
--------------------------------------------------------------------------------
/docs_v2/static/deployment/traefik.toml:
--------------------------------------------------------------------------------
1 | [entryPoints]
2 | [entryPoints.web]
3 | address = ":80"
4 | [entryPoints.webs]
5 | address = ":443"
6 | [entryPoints.rtmp]
7 | address = ":1935"
8 |
9 | [api]
10 | dashboard = true
11 | insecure = true
12 |
13 | [providers.docker]
14 | endpoint = "unix:///var/run/docker.sock"
15 | exposedByDefault = false
16 | swarmMode = true
17 |
18 | [accessLog]
19 | filePath = "/var/log/traefik/access.log"
20 | bufferingSize = 20
21 | [accessLog.fields.headers]
22 | defaultMode = "keep"
23 | [accessLog.fields.headers.names]
24 | "User-Agent" = "keep"
25 | "Authorization" = "drop"
26 | "Content-Type" = "drop"
27 |
28 | [log]
29 | level = "INFO"
30 |
31 | [certificatesResolvers.liveresolver.acme]
32 | email = "some.name@mail.de"
33 | storage = "/acme/acme-v2.json"
34 | [certificatesResolvers.liveresolver.acme.httpChallenge]
35 | entryPoint = "web"
36 |
37 | [metrics]
38 | [metrics.prometheus]
--------------------------------------------------------------------------------
/docs_v2/static/deployment/workers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/static/deployment/workers.png
--------------------------------------------------------------------------------
/docs_v2/static/icons/gocast-gopher-lg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/static/icons/gocast-gopher-lg.png
--------------------------------------------------------------------------------
/docs_v2/static/icons/gocast-gopher-xs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/static/icons/gocast-gopher-xs.png
--------------------------------------------------------------------------------
/docs_v2/static/img/showcase-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/static/img/showcase-01.png
--------------------------------------------------------------------------------
/docs_v2/static/img/showcase-02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/static/img/showcase-02.png
--------------------------------------------------------------------------------
/docs_v2/static/img/showcase-03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/static/img/showcase-03.png
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/deployment/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Deployment 🆕",
3 | "position": 3,
4 | "link": {
5 | "type": "generated-index",
6 | "description": "Connect to the GoCast network using your own server or cloud provider."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/deployment/step-by-step/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Step-by-Step Guide",
3 | "position": 4,
4 | "link": {
5 | "type": "generated-index",
6 | "description": "Connect to GoCast using with your own server or cloud provider. This guide will walk you through the process step-by-step."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/deployment/step-by-step/example-deployment.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Example Deployment"
3 | sidebar_position: 10
4 | description: "Example Deployment."
5 | ---
6 |
7 | # Example Deployment
8 |
9 | Here's an example of how the GoCast network can look like:
10 |
11 | The main instance is managed centrally by TUM, while the other node clusters are connected individually by the organizations.
12 |
13 | Each organization can decide how many resources it wants to allocate to each service depending on their expected usage.
14 |
15 | Optionally, an organization can add additional services, such as the _Voice Service_ for subtitling live streams and VoDs.
16 |
17 | 
18 |
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/features/LectureHallStreams.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Stream from your Lecture Hall"
3 | sidebar_position: 1
4 | description: "Automatic Broadcasting and Recording of Auditoriums"
5 | ---
6 |
7 | ## Automatic Broadcasting and Recording of Auditoriums
8 |
9 | With GoCast, you can easily stream your lectures to the internet. This allows students to follow the lecture from home
10 | or on the go. If you wish so, a recording of the lecture is also available for later viewing.
11 |
12 | For this purpose, we have installed Streaming Media Processors (SMPs) in many lecture halls at TUM. These devices are
13 | capable of capturing the video and audio of the lecture and sending it to our servers for broadcasting.
14 |
15 | For a guide on how to stream from a lecture hall, please refer to the [Lecture Hall Streaming Guide](/docs/usage/lecturehall-streaming.md).
16 |
17 | ---
18 |
19 | # Self-Streaming using OBS, Zoom or other Software
20 |
21 | You can also stream your lectures yourself with any streaming software you like. We recommend OBS for this purpose.
22 |
23 | For instructions on how to self-stream, please refer to the [Self-Streaming Guide](/docs/usage/self-streaming.md).
24 |
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/features/VideoOnDemand.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Video On Demand"
3 | sidebar_position: 2
4 | description: "Upload videos to the server and stream them to the students."
5 | ---
6 |
7 | # Video On Demand
8 |
9 | Video On Demand (VoD) is a feature that allows you to upload videos to the server and stream them to the students. This
10 | feature is useful for hosting videos that you want to share with your students.
11 |
12 | You can also record your live classes and get them automatically uploaded to the server. This way, students who missed
13 | the live class can watch the recording later.
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/features/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Features",
3 | "position": 2,
4 | "link": {
5 | "type": "generated-index",
6 | "description": "Learn about the features of GoCast."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/features/_index.md:
--------------------------------------------------------------------------------
1 | awdawd
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/selfhosting/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Selfhosting",
3 | "position": 4,
4 | "link": {
5 | "type": "generated-index",
6 | "description": "Selfhost GoCast on your network."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/selfhosting/networking.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Networking"
3 | sidebar_position: 3
4 | ---
5 |
6 |
7 | ## Networking
8 |
9 | The following ports need to be exposed to the public:
10 |
11 | | Server (label) | Port |
12 | |----------------------------------|-----------------|
13 | | GoCast Server (tumlive, traefik) | 80 TCP, 443 TCP |
14 | | Worker (worker) | 1935 TCP |
15 | | Edge (edge) | 80 TCP, 443 TCP |
16 |
17 | Between the individual servers, communication should not be firewalled. Auditorium hardware should also be in the same VLAN.
18 |
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/usage/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Usage",
3 | "position": 1,
4 | "link": {
5 | "type": "generated-index",
6 | "description": "Learn how to use GoCast."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/usage/chat-img/activate.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/versioned_docs/version-beta/usage/chat-img/activate.jpg
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/usage/chat-img/anonymous.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/versioned_docs/version-beta/usage/chat-img/anonymous.jpg
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/usage/chat-img/approve.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/versioned_docs/version-beta/usage/chat-img/approve.jpg
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/usage/chat-img/at-ing.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/versioned_docs/version-beta/usage/chat-img/at-ing.jpg
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/usage/chat-img/dismiss.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/versioned_docs/version-beta/usage/chat-img/dismiss.jpg
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/usage/chat-img/emojis.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/versioned_docs/version-beta/usage/chat-img/emojis.jpg
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/usage/chat-img/poll-result.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/versioned_docs/version-beta/usage/chat-img/poll-result.jpg
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/usage/chat-img/polls.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/versioned_docs/version-beta/usage/chat-img/polls.jpg
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/usage/chat-img/resolve.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/versioned_docs/version-beta/usage/chat-img/resolve.jpg
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/usage/chat-img/resolved-with-mark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/versioned_docs/version-beta/usage/chat-img/resolved-with-mark.jpg
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/usage/course-img/course-nav.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/versioned_docs/version-beta/usage/course-img/course-nav.png
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/usage/course-img/lecture-edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/versioned_docs/version-beta/usage/course-img/lecture-edit.png
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/usage/course-img/new-course-prompt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/versioned_docs/version-beta/usage/course-img/new-course-prompt.png
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/usage/course-img/new-course.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/versioned_docs/version-beta/usage/course-img/new-course.png
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/usage/lecturehall-streaming.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Lecture Hall Streaming"
3 | sidebar_position: 3
4 | description: "Streaming from a lecture hall with installed SMPs."
5 | ---
6 |
7 | # Streaming from a lecture hall with installed SMPs
8 |
9 | This guide contains information on how to stream from a lecture hall at TUM.
10 |
11 | ## How to stream
12 |
13 | To stream your lecture from a SMP equipped lecture hall, follow these steps:
14 | 1. Create your course
15 | 2. Create a lecture
16 | 3. Add a room to the lecture
17 | 4. Set the date and time of the lecture (**Important:** Please also set the end time)
18 |
19 | In most cases, your lecture gets imported from TUMOnline. If you want to stream a lecture that is not in TUMOnline,
20 | you can create a new course and lecture in GoCast.
21 |
22 | ## How to create a livestream
23 |
24 | 1. Select `Livestream` and then press `Continue`.
25 | 
26 | 2. Enter your lecture name and select your lecture hall. You also have to insert your lecture start and end.
27 | After these steps, you can press `Create Lecture`.
28 | 
29 | 3. Your lecture will automatically start at the selected time.
30 |
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/usage/video-img/sections-on-watch-page.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/versioned_docs/version-beta/usage/video-img/sections-on-watch-page.jpg
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/usage/video-img/video-sections.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/docs_v2/versioned_docs/version-beta/usage/video-img/video-sections.jpg
--------------------------------------------------------------------------------
/docs_v2/versioned_docs/version-beta/usage/video.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Video"
3 | sidebar_position: 5
4 | description: "Useful information for VoDs."
5 | ---
6 |
7 | # Video
8 |
9 | ## VoD Sections
10 |
11 | Structuring lectures into sections makes lectures more rewatchable.
12 | A click on a section will jump to the given timestamp. Hence, students can repeat
13 | lectures section-wise.
14 |
15 | 
16 |
17 | ## Create Sections
18 |
19 | On the Admin page's sidebar, navigate to:
20 |
21 | `Courses > 'Term' > 'Your Course' - Settings`
22 |
23 | There you will find a list of lectures. Sections can only be added
24 | to VoD streams. (Visualized by the Green VoD Symbol)
25 |
26 | The UI for managing video sections is very intuitive.
27 |
28 | 
--------------------------------------------------------------------------------
/docs_v2/versioned_sidebars/version-beta-sidebars.json:
--------------------------------------------------------------------------------
1 | {
2 | "tutorialSidebar": [
3 | {
4 | "type": "autogenerated",
5 | "dirName": "."
6 | }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/docs_v2/versions.json:
--------------------------------------------------------------------------------
1 | [
2 | "beta"
3 | ]
4 |
--------------------------------------------------------------------------------
/go.work:
--------------------------------------------------------------------------------
1 | go 1.24.0
2 |
3 | use (
4 | .
5 | ./worker
6 | ./worker/edge
7 | runner
8 | vod-service
9 | )
10 |
--------------------------------------------------------------------------------
/init.sql:
--------------------------------------------------------------------------------
1 | CREATE DATABASE IF NOT EXISTS tumlive;
2 |
--------------------------------------------------------------------------------
/model/bookmark.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "gorm.io/gorm"
5 | )
6 |
7 | type Bookmark struct {
8 | gorm.Model
9 |
10 | Description string `gorm:"not null" json:"description"`
11 | Hours uint `gorm:"not null" json:"hours"`
12 | Minutes uint `gorm:"not null" json:"minutes"`
13 | Seconds uint `gorm:"not null" json:"seconds"`
14 | UserID uint `gorm:"not null" json:"-"`
15 | StreamID uint `gorm:"not null" json:"-"`
16 | }
17 |
--------------------------------------------------------------------------------
/model/camera_preset.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | type CameraPreset struct {
4 | Name string `gorm:"not null"`
5 | PresetID int `gorm:"primaryKey;autoIncrement:false"`
6 | Image string
7 | LectureHallID uint `gorm:"primaryKey;autoIncrement:false"`
8 | IsDefault bool // this will be selected if there's no preference
9 | }
10 |
--------------------------------------------------------------------------------
/model/chat_reaction.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | type ChatReaction struct {
4 | ChatID uint `gorm:"primaryKey; not null" json:"chatID"`
5 | UserID uint `gorm:"primaryKey; not null" json:"userID"`
6 | Username string `gorm:"not null" json:"username"`
7 | Emoji string `gorm:"primaryKey; not null" json:"emoji"`
8 | }
9 |
--------------------------------------------------------------------------------
/model/course_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "strings"
5 | "testing"
6 | )
7 |
8 | func TestBeforeSave(t *testing.T) {
9 | course := &Course{}
10 | cases := []struct {
11 | slug string
12 | wantErr bool
13 | }{
14 | {"", true},
15 | {strings.Repeat("a", 300), true},
16 | {"test123", false},
17 | {"test_123", false},
18 | {"!test", true},
19 | {"\" && rm -rf /", true},
20 | }
21 | for _, tc := range cases {
22 | course.Slug = tc.slug
23 | err := course.BeforeSave(nil)
24 | if err == nil && tc.wantErr {
25 | t.Errorf("BeforeCreate(%q) = nil, want error", tc.slug)
26 | } else if err != nil && !tc.wantErr {
27 | t.Errorf("BeforeCreate(%q) = %v, want nil", tc.slug, err)
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/model/email.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | // Email represents an email to be sent.
10 | type Email struct {
11 | gorm.Model
12 |
13 | From string `gorm:"not null"`
14 | To string `gorm:"not null"`
15 | Subject string `gorm:"not null"`
16 | Body string `gorm:"longtext;not null"`
17 | Success bool `gorm:"not null;default:false"`
18 | Retries int `gorm:"not null;default:0"`
19 | LastTry time.Time `gorm:"default:null"`
20 | Errors string `gorm:"longtext;default:null"`
21 | }
22 |
--------------------------------------------------------------------------------
/model/info-page.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "html/template"
5 |
6 | "github.com/microcosm-cc/bluemonday"
7 | "github.com/russross/blackfriday/v2"
8 | "gorm.io/gorm"
9 | )
10 |
11 | type InfoPageType uint
12 |
13 | const (
14 | INFOPAGE_MARKDOWN InfoPageType = iota + 1
15 | )
16 |
17 | type InfoPage struct {
18 | gorm.Model
19 |
20 | Name string `gorm:"not null"` // e.g. 'privacy', 'imprint',...
21 | RawContent string `gorm:"text; not null"`
22 | Type InfoPageType `gorm:"not null; default: 1"`
23 | }
24 |
25 | func (mt InfoPage) Render() template.HTML {
26 | var renderedContent template.HTML
27 | switch mt.Type {
28 | case INFOPAGE_MARKDOWN:
29 | unsafe := blackfriday.Run([]byte(mt.RawContent))
30 | html := bluemonday.
31 | UGCPolicy().
32 | SanitizeBytes(unsafe)
33 | renderedContent = template.HTML(html)
34 | default:
35 | renderedContent = template.HTML(mt.RawContent)
36 | }
37 | return renderedContent
38 | }
39 |
--------------------------------------------------------------------------------
/model/ingest-server.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "gorm.io/gorm"
4 |
5 | // IngestServer represents a server we ingest our streams to. This is used for load balancing.
6 | type IngestServer struct {
7 | gorm.Model `json:"gorm_model"`
8 | Url string `json:"url"` // e.g. rtmp://user:password@ingest1.huge.server.com
9 | OutUrl string `gorm:"not null"` // e.g. https://out.server.com/streams/%s/playlist.m3u8 where %s is the stream name
10 | Workload int `json:"workload,omitempty"` // # of streams currently ingesting to this server
11 | StreamNames []StreamName // array of stream names that will be assigned to this server
12 | }
13 |
--------------------------------------------------------------------------------
/model/lecture_hall_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestBeforeSaveLectureHall(t *testing.T) {
11 | cases := []struct {
12 | l LectureHall
13 | expected LectureHall
14 | wantErr bool
15 | }{
16 | {
17 | l: LectureHall{
18 | CameraIP: "not an ip",
19 | },
20 | wantErr: true,
21 | },
22 | {
23 | l: LectureHall{
24 | CameraIP: "127.0.0.almostanip",
25 | },
26 | expected: LectureHall{
27 | CameraIP: "",
28 | },
29 | wantErr: true,
30 | },
31 | {
32 | l: LectureHall{
33 | CameraIP: "127.0.0.1",
34 | CamIP: "somehost/malicious\" && stuff",
35 | },
36 | expected: LectureHall{
37 | CameraIP: "127.0.0.1",
38 | CamIP: "somehost/malicious%22%20&&%20stuff",
39 | },
40 | wantErr: false,
41 | },
42 | }
43 |
44 | for _, tc := range cases {
45 | t.Run(fmt.Sprintf("BeforeSave(%+v)", tc.l), func(t *testing.T) {
46 | err := tc.l.BeforeSave(nil)
47 | if err != nil && !tc.wantErr {
48 | t.Errorf("BeforeCreateLectureHall(%+v): got error %v, want no error", tc.l, err)
49 | } else if err == nil && tc.wantErr {
50 | t.Errorf("BeforeCreateLectureHall(%+v): got no error, want error", tc.l)
51 | }
52 | if err == nil {
53 | assert.Equal(t, tc.expected, tc.l)
54 | }
55 | })
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/model/model-base.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | // Model is a base model that can be embedded in other models
10 | // it's basically the same as gorm.Model but with convenient json annotations
11 | type Model struct {
12 | ID uint `gorm:"primarykey" json:"id"`
13 | CreatedAt time.Time `json:"createdAt"`
14 | UpdatedAt time.Time `json:"-"`
15 | DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
16 | }
17 |
--------------------------------------------------------------------------------
/model/model_logger.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "log/slog"
5 | "os"
6 | )
7 |
8 | var logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
9 | Level: slog.LevelDebug,
10 | })).With("service", "model")
11 |
--------------------------------------------------------------------------------
/model/poll.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | "gorm.io/gorm"
6 | )
7 |
8 | type Poll struct {
9 | gorm.Model
10 |
11 | StreamID uint // used by gorm
12 | Stream Stream `gorm:"foreignKey:stream_id;not null;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
13 | Question string `gorm:"not null" json:"question"`
14 | Active bool `gorm:"not null;default:true" json:"active"`
15 |
16 | PollOptions []PollOption `gorm:"many2many:chat_poll_options" json:"pollOptions"`
17 | }
18 |
19 | type PollOption struct {
20 | gorm.Model
21 |
22 | Answer string `gorm:"not null" json:"answer"`
23 | Votes []User `gorm:"many2many:poll_option_user_votes" json:"-"`
24 | }
25 |
26 | func (o PollOption) GetStatsMap(votes int64) gin.H {
27 | return gin.H{
28 | "ID": o.ID,
29 | "answer": o.Answer,
30 | "votes": votes,
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/model/progress.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | // StreamProgress represents the progress of a stream or video. Currently, it is only used for VoDs.
4 | type StreamProgress struct {
5 | Progress float64 `gorm:"not null" json:"progress"` // The progress of the stream as represented as a floating point value between 0 and 1.
6 | Watched bool `gorm:"not null;default:false" json:"watched"` // Whether the user has marked the stream as watched.
7 |
8 | // We need to use a primary key in order to use ON CONFLICT in dao/progress.go, same as e.g. https://www.sqlite.org/lang_conflict.html.
9 | StreamID uint `gorm:"primaryKey" json:"streamId"`
10 | UserID uint `gorm:"primaryKey" json:"-"`
11 | }
12 |
--------------------------------------------------------------------------------
/model/register_link.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "gorm.io/gorm"
4 |
5 | type RegisterLink struct {
6 | gorm.Model
7 |
8 | UserID uint `gorm:"not null"`
9 | RegisterSecret string `gorm:"not null"`
10 | }
11 |
--------------------------------------------------------------------------------
/model/search/doc.go:
--------------------------------------------------------------------------------
1 | // Package search contains models that won't be added to the database
2 | // but are persisted in our search backend meilisearch.
3 | package search
4 |
--------------------------------------------------------------------------------
/model/search/prefetchedCourse.go:
--------------------------------------------------------------------------------
1 | package search
2 |
3 | // PrefetchedCourse represents a course we found in tumonline. This course can be found and created through the "create course" user interface
4 | type PrefetchedCourse struct {
5 | CourseID string `json:"courseID,omitempty"`
6 | Name string `json:"name,omitempty"`
7 | Year int `json:"year,omitempty"`
8 | Term string `json:"term,omitempty"` // Either W or S
9 | }
10 |
--------------------------------------------------------------------------------
/model/semester.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | type Semester struct {
4 | TeachingTerm string
5 | Year int
6 | }
7 |
8 | // IsInRangeOfSemesters checks if s is element of semesters slice
9 | func (s *Semester) IsInRangeOfSemesters(semesters []Semester) bool {
10 | for _, semester := range semesters {
11 | if s.Year == semester.Year && s.TeachingTerm == semester.TeachingTerm {
12 | return true
13 | }
14 | }
15 | return false
16 | }
17 |
18 | // IsBetweenSemesters checks if s is between firstSemester (inclusive) and lastSemester (inclusive)
19 | func (s *Semester) IsBetweenSemesters(firstSemester Semester, lastSemester Semester) bool {
20 | if firstSemester.Year == lastSemester.Year && firstSemester.TeachingTerm == lastSemester.TeachingTerm {
21 | return s.Year == firstSemester.Year && s.TeachingTerm == firstSemester.TeachingTerm
22 | }
23 | return s.IsGreaterEqualThan(firstSemester) && lastSemester.IsGreaterEqualThan(*s)
24 | }
25 |
26 | // IsEqual checks if s is equal to otherSemester
27 | func (s *Semester) IsEqual(otherSemester Semester) bool {
28 | return s.Year == otherSemester.Year && s.TeachingTerm == otherSemester.TeachingTerm
29 | }
30 |
31 | // IsGreaterEqualThan checks if s comes after or is equal to s1
32 | func (s *Semester) IsGreaterEqualThan(s1 Semester) bool {
33 | return s.Year > s1.Year || (s.Year == s1.Year && (s.TeachingTerm == "W" || s1.TeachingTerm == "S"))
34 | }
35 |
--------------------------------------------------------------------------------
/model/server-notification.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "errors"
5 | "html/template"
6 | "time"
7 |
8 | "gorm.io/gorm"
9 | )
10 |
11 | // ServerNotification todo: rename to ServerAlert to avoid confusion with Notification
12 | type ServerNotification struct {
13 | gorm.Model
14 |
15 | Text string `gorm:"not null"`
16 | Warn bool `gorm:"not null;default:false"` // if false -> Info
17 | Start time.Time `gorm:"not null"`
18 | Expires time.Time `gorm:"not null"`
19 | }
20 |
21 | func (s ServerNotification) BeforeCreate(tx *gorm.DB) error {
22 | if s.Expires.Before(s.Start) {
23 | return errors.New("can't save notification where expires is before start")
24 | }
25 | return nil
26 | }
27 |
28 | func (s ServerNotification) FormatFrom() string {
29 | return s.Start.Format("2006-01-02 15:04")
30 | }
31 |
32 | func (s ServerNotification) FormatExpires() string {
33 | return s.Expires.Format("2006-01-02 15:04")
34 | }
35 |
36 | func (s ServerNotification) HTML() template.HTML {
37 | return template.HTML(s.Text)
38 | }
39 |
--------------------------------------------------------------------------------
/model/shortlink.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "gorm.io/gorm"
4 |
5 | // ShortLink friendly name for a link to courses highlight page
6 | type ShortLink struct {
7 | gorm.Model
8 |
9 | Link string `gorm:"type:varchar(256); unique; not null"`
10 | CourseId uint `gorm:"not null"`
11 | }
12 |
--------------------------------------------------------------------------------
/model/silence.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "gorm.io/gorm"
4 |
5 | type Silence struct {
6 | gorm.Model `json:"omitempty"`
7 |
8 | Start uint `json:"start"`
9 | End uint `json:"end"`
10 | StreamID uint `json:"stream_id,omitempty"`
11 | }
12 |
--------------------------------------------------------------------------------
/model/stat.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | type Stat struct {
10 | gorm.Model
11 |
12 | Time time.Time `gorm:"not null"`
13 | StreamID uint `gorm:"not null"`
14 | Viewers uint `gorm:"not null;default:0"`
15 | Live bool `gorm:"not null;default:false"`
16 | }
17 |
--------------------------------------------------------------------------------
/model/stream-name.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | // StreamName is essentially a "streaming slot" used for load balancing
10 | type StreamName struct {
11 | gorm.Model
12 |
13 | StreamName string `gorm:"type:varchar(64); unique; not null"`
14 | IsTranscoding bool `gorm:"not null;default:false"`
15 | IngestServerID uint `gorm:"not null"`
16 | StreamID uint // Is null when the slot is not used
17 | FreedAt time.Time `gorm:"not null;default:0"`
18 | }
19 |
--------------------------------------------------------------------------------
/model/stream-unit.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "fmt"
5 | "html/template"
6 |
7 | "github.com/microcosm-cc/bluemonday"
8 | "github.com/russross/blackfriday/v2"
9 | "gorm.io/gorm"
10 | )
11 |
12 | type StreamUnit struct {
13 | gorm.Model
14 |
15 | UnitName string
16 | UnitDescription string
17 | UnitStart uint `gorm:"not null"`
18 | UnitEnd uint `gorm:"not null"`
19 | StreamID uint `gorm:"not null"`
20 | }
21 |
22 | func (s StreamUnit) GetUnitDurationMS() uint {
23 | return s.UnitEnd - s.UnitStart
24 | }
25 |
26 | func (s StreamUnit) GetRoundedUnitLen() string {
27 | lenS := (s.UnitEnd - s.UnitStart) / 1000
28 | lenM := lenS / 60
29 | lenH := lenM / 60
30 | lenM %= 60
31 | lenS %= 60
32 | if lenH > 0 {
33 | return fmt.Sprintf("%2dh, %2dmin", lenH, lenM)
34 | }
35 | return fmt.Sprintf("%2dmin, %2dsec", lenM, lenS)
36 | }
37 |
38 | func (s StreamUnit) GetDescriptionHTML() template.HTML {
39 | unsafe := blackfriday.Run([]byte(s.UnitDescription))
40 | html := bluemonday.
41 | UGCPolicy().
42 | AddTargetBlankToFullyQualifiedLinks(true).
43 | SanitizeBytes(unsafe)
44 | return template.HTML(html)
45 | }
46 |
--------------------------------------------------------------------------------
/model/subtitles.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "gorm.io/gorm"
5 | )
6 |
7 | // Subtitles represents subtitles for a particular stream in a particular language
8 | type Subtitles struct {
9 | gorm.Model
10 |
11 | StreamID uint `gorm:"not null"`
12 | Content string `gorm:"not null"` // the .srt content provided by the voice-service
13 | Language string `gorm:"not null"`
14 | }
15 |
16 | // TableName returns the name of the table for the Subtitles model in the database.
17 | func (*Subtitles) TableName() string {
18 | return "subtitles"
19 | }
20 |
21 | // BeforeCreate is currently not implemented for Subtitles
22 | func (s *Subtitles) BeforeCreate(tx *gorm.DB) (err error) {
23 | return nil
24 | }
25 |
26 | // AfterFind is currently not implemented for Subtitles
27 | func (s *Subtitles) AfterFind(tx *gorm.DB) (err error) {
28 | return nil
29 | }
30 |
--------------------------------------------------------------------------------
/model/token.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "database/sql"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | const (
10 | TokenScopeAdmin = "admin"
11 | TokenScopeLecturer = "lecturer"
12 | )
13 |
14 | // Token can be used to authenticate instead of a user account
15 | type Token struct {
16 | gorm.Model
17 | UserID uint // used by gorm
18 | User User `gorm:"foreignKey:user_id;not null;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` // creator of the token
19 | Token string `json:"token" gorm:"not null"` // secret token
20 | Expires sql.NullTime `json:"expires"` // expiration date (null if none)
21 | Scope string `json:"scope" gorm:"not null"` // scope of the token, currently only admin
22 | LastUse sql.NullTime `json:"last_use"` // last time the token was used
23 | }
24 |
--------------------------------------------------------------------------------
/model/transcoding-failure.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "gorm.io/gorm"
4 |
5 | // TranscodingFailure represents a failed transcoding attempt
6 | type TranscodingFailure struct {
7 | gorm.Model
8 |
9 | StreamID uint `gorm:"not null"`
10 | Stream Stream
11 | Version StreamVersion `gorm:"not null"`
12 | Logs string `gorm:"not null"`
13 | ExitCode int
14 | FilePath string `gorm:"not null"` // the source file that could not be transcoded
15 | Hostname string `gorm:"not null"` // the hostname of the worker that failed
16 |
17 | // Ignored by gorm:
18 | FriendlyTime string `gorm:"-"`
19 | }
20 |
21 | func (t *TranscodingFailure) AfterFind(tx *gorm.DB) (err error) {
22 | t.FriendlyTime = t.CreatedAt.Format("02.01.2006 15:04")
23 | return nil
24 | }
25 |
--------------------------------------------------------------------------------
/model/transcodingProgress.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | type StreamVersion string
4 |
5 | const (
6 | COMB StreamVersion = "COMB"
7 | CAM StreamVersion = "CAM"
8 | PRES StreamVersion = "PRES"
9 | )
10 |
11 | // TranscodingProgress is the progress as a percentage of the conversion of a single stream view (e.g. stream 123, COMB view)
12 | type TranscodingProgress struct {
13 | StreamID uint `gorm:"primaryKey" json:"streamID"`
14 | Version StreamVersion `gorm:"primaryKey" json:"version"`
15 |
16 | Progress int `gorm:"not null; default:0" json:"progress"`
17 | }
18 |
--------------------------------------------------------------------------------
/model/upload-key.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "gorm.io/gorm"
4 |
5 | type VideoType string
6 |
7 | const (
8 | VideoTypeCombined VideoType = "COMB"
9 | VideoTypePresentation VideoType = "PRES"
10 | VideoTypeCamera VideoType = "CAM"
11 | )
12 |
13 | func (v VideoType) Valid() bool {
14 | return v == VideoTypeCombined || v == VideoTypePresentation || v == VideoTypeCamera
15 | }
16 |
17 | // UploadKey represents a key that is created when a user uploads a file,
18 | // sent to the worker with the upload request and back to TUM-Live to authenticate the request.
19 | type UploadKey struct {
20 | gorm.Model
21 | UploadKey string `gorm:"not null"`
22 | Stream Stream
23 | StreamID uint
24 | VideoType VideoType `gorm:"not null"`
25 | }
26 |
--------------------------------------------------------------------------------
/model/video-section.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "gorm.io/gorm"
5 | )
6 |
7 | type VideoSection struct {
8 | gorm.Model
9 |
10 | Description string `gorm:"not null;index:,class:FULLTEXT" json:"description"`
11 | StartHours uint `gorm:"not null" json:"startHours"`
12 | StartMinutes uint `gorm:"not null" json:"startMinutes"`
13 | StartSeconds uint `gorm:"not null" json:"startSeconds"`
14 |
15 | StreamID uint `gorm:"not null" json:"streamID"`
16 | FileID uint `gorm:"not null" json:"fileID"`
17 | }
18 |
--------------------------------------------------------------------------------
/model/video-seek-chunk.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | type VideoSeekChunk struct {
4 | ChunkIndex uint `gorm:"primaryKey;autoIncrement:false" json:"chunkIndex"`
5 | Hits uint `gorm:"not null" json:"hits"`
6 | StreamID uint `gorm:"primaryKey;autoIncrement:false" json:"streamID"`
7 | }
8 |
--------------------------------------------------------------------------------
/model/worker.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "time"
4 |
5 | type Worker struct {
6 | WorkerID string `gorm:"primaryKey"`
7 | Host string
8 | Status string
9 | Workload uint // How much the worker has to do. +1 per silence detection job, +2 per converting job, +3 per streaming job
10 | LastSeen time.Time
11 |
12 | // VM stats:
13 | CPU string
14 | Memory string
15 | Disk string
16 | Uptime string
17 |
18 | Version string
19 | }
20 |
21 | func (w *Worker) IsAlive() bool {
22 | return w.LastSeen.After(time.Now().Add(time.Minute * -6))
23 | }
24 |
--------------------------------------------------------------------------------
/persist.gob:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/persist.gob
--------------------------------------------------------------------------------
/pkg/runner_manager/manager_test.go:
--------------------------------------------------------------------------------
1 | package runner_manager
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestManagerOptions(t *testing.T) {
8 | m := Manager{}
9 | m.applyOpts([]Option{WithListenAddr(":1")})
10 | if m.listenAddr != ":1" {
11 | t.Errorf("m.listenAddr want: %v have: %v", ":1", m.listenAddr)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base",
5 | "group:allNonMajor"
6 | ],
7 | "ignoreDeps": ["github.com/TUM-Dev/gocast/worker", "github.com/TUM-Dev/gocast/runner"],
8 | "enabledManagers": ["npm", "gomod"]
9 | }
10 |
--------------------------------------------------------------------------------
/rtmp-proxy/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM tiangolo/nginx-rtmp
2 |
3 | # Install jq and ffmpeg
4 | RUN apt-get update && apt-get install -y jq ffmpeg
5 |
6 | # Copy preconfigured nginx.conf and publish.sh into the container
7 | COPY nginx.conf /etc/nginx/nginx.conf
8 | COPY publish.sh /etc/nginx/scripts/publish.sh
9 |
10 | # Make publish.sh executable
11 | RUN chmod +x /etc/nginx/scripts/publish.sh
--------------------------------------------------------------------------------
/rtmp-proxy/README.md:
--------------------------------------------------------------------------------
1 | # TODO
--------------------------------------------------------------------------------
/rtmp-proxy/nginx.conf:
--------------------------------------------------------------------------------
1 | worker_processes 1;
2 |
3 | events {
4 | worker_connections 1024;
5 | }
6 |
7 | rtmp {
8 | server {
9 | listen 1935;
10 | chunk_size 4096;
11 |
12 | application live {
13 | live on;
14 | record off;
15 |
16 | # Call script when the stream is published
17 | exec_publish /etc/nginx/scripts/publish.sh $name;
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/rtmp-proxy/publish.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | LOG_FILE="/tmp/rtmp_exec.log" # TODO: change if necessary
4 |
5 | # Extract the stream key (first argument)
6 | STREAM_KEY="$1"
7 |
8 | # echo "Received stream key: $STREAM_KEY" >> $LOG_FILE 2>&1 # TODO: for debugging only
9 |
10 | # Exchange the stream key for a stream-specific URL
11 | API_URL="https://live.rbg.tum.de/api/token/proxy/$STREAM_KEY"
12 | RELAY_URL=$(curl -s -X POST "$API_URL" | jq -r '.url')
13 |
14 | if [ -z "$RELAY_URL" ]; then
15 | echo "No relay URL found for stream key" >> $LOG_FILE 2>&1
16 | exit 1
17 | fi
18 | echo "Relay URL found: [$RELAY_URL]" >> $LOG_FILE 2>&1
19 |
20 | # Push the stream to the stream specific RTMP URL using ffmpeg
21 | ffmpeg -re -i rtmp://localhost:1935/live/$STREAM_KEY -c copy -f flv "$RELAY_URL" >> $LOG_FILE 3>&1
--------------------------------------------------------------------------------
/runner/.gitignore:
--------------------------------------------------------------------------------
1 | mediamtx
2 | storage
3 |
--------------------------------------------------------------------------------
/runner/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM amd64/golang:1.23-alpine3.22 as builder
2 |
3 | WORKDIR /go/src/github.com/TUM-Dev/gocast/runner
4 | COPY . .
5 |
6 | RUN GO111MODULE=on go mod download
7 | # bundle version into binary if specified in build-args, dev otherwise.
8 | ARG version=dev
9 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags "-w -extldflags '-static' -X main.V=${version}" -o /runner cmd/runner/main.go
10 |
11 | FROM bluenviron/mediamtx:1.1.0 as mediamtx
12 |
13 | FROM alpine:3.18
14 | ADD entrypoint.sh /entrypoint.sh
15 | ADD mediamtx.yml /mediamtx.yml
16 | RUN chmod +x /entrypoint.sh
17 |
18 | RUN apk add --no-cache \
19 | ffmpeg \
20 | tzdata
21 |
22 | COPY --from=builder /runner /runner
23 | RUN chmod +x /runner
24 | COPY --from=mediamtx /mediamtx /mediamtx
25 | RUN chmod +x /mediamtx
26 |
27 | CMD ["/entrypoint.sh"]
--------------------------------------------------------------------------------
/runner/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: all
2 | all: build
3 |
4 | VERSION := $(shell git rev-parse --short origin/HEAD)
5 |
6 | .PHONY: protoGen
7 | protoGen:
8 | protoc ./*.proto --go-grpc_out=.. --go_out=..
9 |
10 | .PHONY: build
11 | build: deps
12 | go build -o main -ldflags="-X 'main.V=$(VERSION)'" cmd/runner/main.go
13 |
14 | .PHONY: deps
15 | deps:
16 | go get ./...
17 |
18 | .PHONY: install
19 | install:
20 | mv main /bin/runner
21 |
22 | .PHONY: clean
23 | clean:
24 | rm -f main
25 |
26 | .PHONY: test
27 | test:
28 | go test -race ./...
29 |
30 | .PHONY: run
31 | run:
32 | go run cmd/runner/main.go
33 |
34 | .PHONY: lint
35 | lint:
36 | golangci-lint run && protolint lint runner.proto notifications.proto
37 |
--------------------------------------------------------------------------------
/runner/commons.proto:
--------------------------------------------------------------------------------
1 | edition = "2023";
2 |
3 | package protobuf;
4 | option go_package = "runner/protobuf";
5 |
6 | enum StreamVersion {
7 | STREAM_VERSION_UNSPECIFIED = 0;
8 | STREAM_VERSION_COMBINED = 1;
9 | STREAM_VERSION_CAMERA = 2;
10 | STREAM_VERSION_PRESENTATION = 3;
11 | }
12 |
--------------------------------------------------------------------------------
/runner/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "log/slog"
5 |
6 | "github.com/caarlos0/env"
7 | )
8 |
9 | var Config struct {
10 | LogFmt string `env:"LOG_FMT" envDefault:"txt"`
11 | LogLevel string `env:"LOG_LEVEL" envDefault:"debug"`
12 | Port int `env:"PORT" envDefault:"52735"`
13 | StoragePath string `env:"STORAGE_PATH" envDefault:"storage/mass"`
14 | SegmentPath string `env:"SEGMENT_PATH" envDefault:"storage/live"`
15 | ErrorPath string `env:"ERROR_PATH" envDefault:"storage/errors"`
16 | GocastServer string `env:"GOCAST_SERVER" envDefault:"localhost:50056"`
17 | Hostname string `env:"REALHOST" envDefault:"localhost"`
18 | EdgeServer string `env:"EDGE_SERVER" envDefault:"http://localhost:8089"`
19 | }
20 |
21 | func init() {
22 | if err := env.Parse(&Config); err != nil {
23 | slog.Error("error parsing envConfig", "error", err)
24 | }
25 |
26 | slog.Info("envConfig loaded", "envConfig", Config)
27 | }
28 |
--------------------------------------------------------------------------------
/runner/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | exec /mediamtx &
4 | exec /runner
5 |
--------------------------------------------------------------------------------
/runner/hls.go:
--------------------------------------------------------------------------------
1 | package runner
2 |
3 | import (
4 | "log/slog"
5 | "net/http"
6 | )
7 |
8 | type HLSServer struct {
9 | log *slog.Logger
10 | fs http.Handler
11 |
12 | version string
13 | }
14 |
15 | func NewHLSServer(liveDir string, log *slog.Logger, version string) *HLSServer {
16 | return &HLSServer{fs: http.FileServer(http.Dir(liveDir)), log: log, version: version}
17 | }
18 |
19 | func (h *HLSServer) Start() error {
20 | http.Handle("/", h)
21 | h.log.Info("starting hls server", "port", 8187)
22 | return http.ListenAndServe(":8187", h)
23 | }
24 |
25 | func (h *HLSServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
26 | h.fs.ServeHTTP(w, r)
27 | }
28 |
--------------------------------------------------------------------------------
/runner/notifications.proto:
--------------------------------------------------------------------------------
1 | edition = "2023";;
2 |
3 | package protobuf;
4 | option go_package = "runner/protobuf";
5 |
6 | import "commons.proto";
7 |
8 | message Notification {
9 | oneof data {
10 | StreamStartNotification stream_start = 1;
11 | StreamEndNotification stream_end = 2;
12 | HeartbeatNotification heartbeat = 3;
13 | VODReadyNotification vod_ready = 4;
14 | }
15 | }
16 |
17 | message StreamInfo {
18 | uint64 id = 1;
19 | }
20 |
21 | message StreamStartNotification {
22 | StreamInfo stream = 1;
23 | string url = 2;
24 | StreamVersion stream_version = 3;
25 | }
26 |
27 | message StreamEndNotification {
28 | StreamInfo stream = 1;
29 | }
30 |
31 | message HeartbeatNotification {
32 | string hostname = 1;
33 | bool draining = 2;
34 | uint64 job_count = 3;
35 | }
36 |
37 | message VODReadyNotification {
38 | StreamInfo stream = 1;
39 | StreamVersion stream_version = 2;
40 | string url = 3;
41 | }
42 |
43 | message NotificationResponse {
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/runner/pkg/actions/actions.go:
--------------------------------------------------------------------------------
1 | package actions
2 |
3 | import (
4 | "context"
5 | "log/slog"
6 |
7 | "github.com/tum-dev/gocast/runner/pkg/metrics"
8 | "github.com/tum-dev/gocast/runner/protobuf"
9 | )
10 |
11 | // Action represents a computation the runner executes.
12 | //
13 | // An action takes a context ctx, may cancel the action.
14 | // Actions should use log for logging and notify for sending messages like their progress to gocast.
15 | // d contains data passed to the action and is used to pass data to the next actions.
16 | // Any error, the action returns will be logged. If that error is an AbortingError, the subsequent actions will be skipped.
17 | type Action func(ctx context.Context, log *slog.Logger, notify chan *protobuf.Notification, d map[string]any, metrics *metrics.Broker) error
18 |
--------------------------------------------------------------------------------
/runner/pkg/actions/error.go:
--------------------------------------------------------------------------------
1 | package actions
2 |
3 | var _ isAbortingError = (*abortingError)(nil)
4 |
5 | type abortingError struct {
6 | error
7 | }
8 |
9 | type isAbortingError interface {
10 | IsAbortingError()
11 | }
12 |
13 | func (e *abortingError) IsAbortingError() {}
14 |
15 | // AbortingError marks an error as aborting, thus skipping all following actions.
16 | func AbortingError(err error) error {
17 | if err == nil {
18 | return nil
19 | }
20 | return &abortingError{err}
21 | }
22 |
23 | // Unwrap implements error wrapping.
24 | func (e *abortingError) Unwrap() error {
25 | return e.error
26 | }
27 |
28 | // Error returns the error string.
29 | func (e *abortingError) Error() string {
30 | if e.error == nil {
31 | return "aborting: "
32 | }
33 | return "aborting: " + e.error.Error()
34 | }
35 |
36 | func IsAbortingError(err error) bool {
37 | _, ok := err.(isAbortingError)
38 | return ok
39 | }
40 |
--------------------------------------------------------------------------------
/runner/pkg/actions/error_test.go:
--------------------------------------------------------------------------------
1 | package actions
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func TestAbortingError(t *testing.T) {
9 | err := AbortingError(fmt.Errorf("some error"))
10 | if err == nil {
11 | t.Errorf("error should not be nil")
12 | }
13 | if err.Error() != "aborting: some error" {
14 | t.Errorf("error should be 'aborting: some error'")
15 | }
16 | if !IsAbortingError(err) {
17 | t.Errorf("error should be retryable")
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/runner/pkg/actions/mkvod_test.go:
--------------------------------------------------------------------------------
1 | package actions
2 |
3 | import (
4 | "strings"
5 | "testing"
6 | )
7 |
8 | func TestConvertHLSPlaylist(t *testing.T) {
9 | input := `#EXTM3U
10 | #EXT-X-VERSION:3
11 | #EXT-X-TARGETDURATION:2
12 | #EXT-X-MEDIA-SEQUENCE:0
13 | #EXT-X-PLAYLIST-TYPE:EVENT
14 | #EXTINF:2.000000,
15 | 00000.ts`
16 | expected := `#EXTM3U
17 | #EXT-X-VERSION:3
18 | #EXT-X-TARGETDURATION:2
19 | #EXT-X-MEDIA-SEQUENCE:0
20 | #EXT-X-PLAYLIST-TYPE:VOD
21 | #EXTINF:2.000000,
22 | 00000.ts
23 | #EXT-X-ENDLIST`
24 |
25 | output := vodFromEventPlst(strings.NewReader(input))
26 | if output != expected {
27 | t.Errorf("Expected:\n%s\nGot:\n%s", expected, output)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/runner/pkg/actions/stream_end.go:
--------------------------------------------------------------------------------
1 | package actions
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log/slog"
7 |
8 | "github.com/tum-dev/gocast/runner/pkg/metrics"
9 | "github.com/tum-dev/gocast/runner/pkg/ptr"
10 | "github.com/tum-dev/gocast/runner/protobuf"
11 | )
12 |
13 | // StreamEnd is an action who's sole purpose is to notify gocast about the end of a stream.
14 | // the only reason it is a separate action is to avoid sending unnecessary
15 | // stream_end notifications if Stream errors.
16 | func StreamEnd(_ context.Context, _ *slog.Logger, notify chan *protobuf.Notification, d map[string]any, metrics *metrics.Broker) error {
17 | streamID, ok := d["streamID"].(uint64)
18 | if !ok {
19 | return AbortingError(fmt.Errorf("no stream id in context"))
20 | }
21 | notify <- &protobuf.Notification{
22 | Data: &protobuf.Notification_StreamEnd{
23 | StreamEnd: &protobuf.StreamEndNotification{
24 | Stream: &protobuf.StreamInfo{Id: ptr.Take(streamID)},
25 | },
26 | },
27 | }
28 | return nil
29 | }
30 |
--------------------------------------------------------------------------------
/runner/pkg/metrics/broker_test.go:
--------------------------------------------------------------------------------
1 | package metrics
2 |
3 | import "testing"
4 |
5 | func TestLabelBuilder(t *testing.T) {
6 | b := NewBroker()
7 | if b == nil {
8 | t.Errorf("broker unexpectedly nil")
9 | return
10 | }
11 | if b.StreamErrors == nil {
12 | t.Errorf("broker metric not initialized")
13 | }
14 | labels := b.With().Stream(123).Source("test").L()
15 | if labels["stream_id"] != "123" || labels["source"] != "test" {
16 | t.Errorf("Unexpected labels: %+v", labels)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/runner/pkg/netutil/netutil.go:
--------------------------------------------------------------------------------
1 | package netutil
2 |
3 | import (
4 | "log/slog"
5 | "net"
6 | "os"
7 | )
8 |
9 | var logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
10 | Level: slog.LevelDebug,
11 | })).With("service", "netutil")
12 |
13 | // GetFreePort returns a free port for tcp use.
14 | func GetFreePort() (port int, err error) {
15 | var a *net.TCPAddr
16 | if a, err = net.ResolveTCPAddr("tcp", "localhost:0"); err == nil {
17 | var l *net.TCPListener
18 | if l, err = net.ListenTCP("tcp", a); err == nil {
19 | defer func(l *net.TCPListener) {
20 | err := l.Close()
21 | if err != nil {
22 | logger.Error("failed to close listener: %v", "err", err)
23 | }
24 | }(l)
25 | return l.Addr().(*net.TCPAddr).Port, nil
26 | }
27 | }
28 | return port, err
29 | }
30 |
--------------------------------------------------------------------------------
/runner/pkg/ptr/ptr.go:
--------------------------------------------------------------------------------
1 | package ptr
2 |
3 | // Take returns a pointer to the given value.
4 | func Take[T any](t T) *T {
5 | return &t
6 | }
7 |
--------------------------------------------------------------------------------
/tools/api-errors.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | type RequestError struct {
10 | Status int
11 | CustomMessage string
12 | Err error
13 | }
14 |
15 | func (r RequestError) Error() string {
16 | if r.Err != nil {
17 | return r.Err.Error()
18 | }
19 | return ""
20 | }
21 |
22 | func (r RequestError) ToResponse() gin.H {
23 | res := gin.H{"status": r.Status, "message": r.CustomMessage}
24 |
25 | if r.Err != nil {
26 | res["error"] = r.Error()
27 | }
28 |
29 | return res
30 | }
31 |
32 | func ErrorHandler(c *gin.Context) {
33 | c.Next()
34 | if len(c.Errors) > 0 {
35 | err := c.Errors[0]
36 | switch tErr := err.Err.(type) {
37 | case RequestError:
38 | c.Errors = []*gin.Error{} // clear errors so they don't get logged
39 | c.JSON(tErr.Status, tErr.ToResponse())
40 | default:
41 | c.Errors = []*gin.Error{} // clear errors so they don't get logged
42 | c.JSON(http.StatusInternalServerError, err.Err.Error())
43 | }
44 | c.Abort()
45 | return
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tools/bot/bot_logger.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | "log/slog"
5 | "os"
6 | )
7 |
8 | var logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
9 | Level: slog.LevelDebug,
10 | })).With("service", "bot")
11 |
--------------------------------------------------------------------------------
/tools/branding.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/spf13/viper"
7 | )
8 |
9 | var BrandingCfg Branding
10 |
11 | type Branding struct {
12 | Title string `yaml:"title"`
13 | Description string `yaml:"description"`
14 | }
15 |
16 | // getDefaultBranding returns the struct branding with default values
17 | func getDefaultBranding() Branding {
18 | return Branding{
19 | Title: "TUM-Live",
20 | Description: "TUM-Live, the livestreaming and VoD service of the " +
21 | "Rechnerbetriebsgruppe at the department of informatics and " +
22 | "mathematics at the Technical University of Munich",
23 | }
24 | }
25 |
26 | // InitBranding initializes the global branding configuration variable `BrandingCfg`. If the config file doesn't exist
27 | // it will be set to the result of `getDefaultBranding()`.
28 | func InitBranding() {
29 | v := viper.New()
30 | v.SetConfigName("branding")
31 | v.SetConfigType("yaml")
32 | v.AddConfigPath("/etc/TUM-Live/")
33 | v.AddConfigPath("$HOME/.TUM-Live")
34 | v.AddConfigPath(".")
35 |
36 | branding := getDefaultBranding()
37 |
38 | err := v.ReadInConfig()
39 | if err == nil {
40 | err = v.Unmarshal(&branding)
41 | logger.Info("Using branding.yaml.")
42 | if err != nil {
43 | panic(fmt.Errorf("fatal error branding file: %v", err))
44 | }
45 | }
46 |
47 | BrandingCfg = branding
48 | }
49 |
--------------------------------------------------------------------------------
/tools/cache.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/dgraph-io/ristretto/v2"
7 | )
8 |
9 | var cache *ristretto.Cache[string, any]
10 |
11 | func initCache() {
12 | c, err := ristretto.NewCache[string, any](&ristretto.Config[string, any]{
13 | NumCounters: 1e6,
14 | MaxCost: 1 << 29,
15 | BufferItems: 64,
16 | })
17 | if err != nil {
18 | panic(err)
19 | }
20 | cache = c
21 | }
22 |
23 | // GetCacheItem returns the value of the key if it exists in the cache. (nil, err) otherwise
24 | func GetCacheItem(key string) (interface{}, bool) {
25 | return cache.Get(key)
26 | }
27 |
28 | // SetCacheItem adds the key and value to the cache with the given expiration time.
29 | func SetCacheItem(key string, value interface{}, ttl time.Duration) {
30 | cache.SetWithTTL(key, value, 1, ttl)
31 | }
32 |
--------------------------------------------------------------------------------
/tools/camera/camera_logger.go:
--------------------------------------------------------------------------------
1 | package camera
2 |
3 | import (
4 | "log/slog"
5 | "os"
6 | )
7 |
8 | var logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
9 | Level: slog.LevelDebug,
10 | })).With("service", "camera")
11 |
--------------------------------------------------------------------------------
/tools/canonical.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "path"
5 | "strconv"
6 | )
7 |
8 | type CanonicalURL struct {
9 | url string
10 | }
11 |
12 | func NewCanonicalURL(url string) CanonicalURL {
13 | return CanonicalURL{url: url}
14 | }
15 |
16 | func (c CanonicalURL) Root() string {
17 | return c.url
18 | }
19 |
20 | func (c CanonicalURL) Course(year int, term string, slug string) string {
21 | return path.Join(c.url, "course", strconv.Itoa(year), term, slug)
22 | }
23 |
24 | func (c CanonicalURL) Stream(slug string, id uint, version string) string {
25 | return path.Join(c.url, "w", slug, strconv.Itoa(int(id)), version)
26 | }
27 |
28 | func (c CanonicalURL) Login() string {
29 | return path.Join(c.url, "login")
30 | }
31 |
32 | func (c CanonicalURL) Info(version string) string {
33 | return path.Join(c.url, version)
34 | }
35 |
--------------------------------------------------------------------------------
/tools/cron.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import "github.com/robfig/cron/v3"
4 |
5 | type CronService struct {
6 | cronJobs map[string]func()
7 | cron *cron.Cron
8 | }
9 |
10 | // Cron is the global CronService
11 | var Cron *CronService
12 |
13 | // InitCronService creates an instance of CronService
14 | func InitCronService() {
15 | Cron = &CronService{
16 | cronJobs: make(map[string]func(), 0),
17 | cron: cron.New(),
18 | }
19 | }
20 |
21 | // AddFunc creates a cronJob fn running at the interval specified by spec. The job can be referenced by name.
22 | func (c *CronService) AddFunc(name string, fn func(), spec string) error {
23 | c.cronJobs[name] = fn
24 | _, err := c.cron.AddFunc(spec, fn)
25 | return err
26 | }
27 |
28 | // Run starts the CronService
29 | func (c *CronService) Run() {
30 | c.cron.Start()
31 | }
32 |
33 | // RunJob executes the cronJob identified by name even when it's not due.
34 | // Invalid names are ignored silently.
35 | func (c *CronService) RunJob(name string) {
36 | if job, ok := c.cronJobs[name]; ok {
37 | go job()
38 | }
39 | }
40 |
41 | // ListCronJobs returns a []string with the names of all cronjobs
42 | func (c *CronService) ListCronJobs() []string {
43 | var l []string
44 | for job := range c.cronJobs {
45 | l = append(l, job)
46 | }
47 | return l
48 | }
49 |
--------------------------------------------------------------------------------
/tools/functions_test.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestMaskEmail(t *testing.T) {
8 | email := "charly@example.com"
9 | expected := "c*****@example.com"
10 | masked, err := MaskEmail(email)
11 | if err != nil {
12 | t.Errorf("MaskEmail(%s) failed: %s", email, err)
13 | }
14 | if masked != expected {
15 | t.Errorf("MaskEmail(%s) = %s, want %s", email, masked, expected)
16 | }
17 | }
18 |
19 | func TestMaskEmailInvalid(t *testing.T) {
20 | vals := []string{"charly@", "charly@e", "@example.com", "charlyexample.com", ""}
21 | for _, val := range vals {
22 | masked, err := MaskEmail(val)
23 | if err == nil {
24 | t.Errorf("MaskEmail(%s) should fail but returned: %s", val, masked)
25 | }
26 | }
27 | }
28 |
29 | func TestMaskLogin(t *testing.T) {
30 | exp := "ge**tum"
31 | if masked := MaskLogin("ge12tum"); masked != exp {
32 | t.Errorf("MaskLogin(ge12tum) = %s, want %s", masked, "ge**tum")
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tools/json-generator.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "github.com/TUM-Dev/gocast/model"
5 | "github.com/gin-gonic/gin"
6 | )
7 |
8 | // AdminCourseJson is the JSON representation of a courses streams for the admin panel
9 | func AdminCourseJson(c *model.Course, lhs []model.LectureHall, u *model.User) []gin.H {
10 | var res []gin.H
11 | streams := c.Streams
12 | for _, s := range streams {
13 | err := SetSignedPlaylists(&s, u, true)
14 | if err != nil {
15 | logger.Error("Could not sign playlist for admin", "err", err)
16 | }
17 | res = append(res, s.GetJson(lhs, *c))
18 | }
19 | return res
20 | }
21 |
--------------------------------------------------------------------------------
/tools/pathprovider/pathprovider.go:
--------------------------------------------------------------------------------
1 | package pathprovider
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | )
8 |
9 | // TUMLiveTemporary is the path at which temporary files like in-progress thumbnails are stored.
10 | var TUMLiveTemporary = filepath.Join(os.TempDir(), "TUM-Live")
11 |
12 | // LiveThumbnail returns the path to the thumbnail of a livestream.
13 | func LiveThumbnail(streamID string) string {
14 | return filepath.Join(TUMLiveTemporary, fmt.Sprintf("%s.jpeg", streamID))
15 | }
16 |
--------------------------------------------------------------------------------
/tools/realtime/client-store.go:
--------------------------------------------------------------------------------
1 | package realtime
2 |
3 | import (
4 | "sync"
5 |
6 | "github.com/google/uuid"
7 | )
8 |
9 | type ClientStore struct {
10 | clients map[string]*Client
11 | mutex sync.RWMutex
12 | }
13 |
14 | func (c *ClientStore) init() {
15 | c.clients = map[string]*Client{}
16 | }
17 |
18 | func (c *ClientStore) NextId() string {
19 | uuidGen, _ := uuid.NewUUID()
20 | uuidString := uuidGen.String()
21 | if _, ok := c.clients[uuidString]; ok {
22 | return c.NextId()
23 | }
24 | return uuidString
25 | }
26 |
27 | func (c *ClientStore) Join(client *Client) {
28 | c.mutex.Lock()
29 | defer c.mutex.Unlock()
30 | if client.Id == "" {
31 | client.Id = c.NextId()
32 | }
33 | c.clients[client.Id] = client
34 | }
35 |
36 | func (c *ClientStore) Exists(id string) bool {
37 | c.mutex.Lock()
38 | defer c.mutex.Unlock()
39 | _, exists := c.clients[id]
40 | return exists
41 | }
42 |
43 | func (c *ClientStore) Get(id string) *Client {
44 | c.mutex.Lock()
45 | defer c.mutex.Unlock()
46 | return c.clients[id]
47 | }
48 |
49 | func (c *ClientStore) Remove(id string) {
50 | c.mutex.Lock()
51 | defer c.mutex.Unlock()
52 | delete(c.clients, id)
53 | }
54 |
--------------------------------------------------------------------------------
/tools/realtime/client.go:
--------------------------------------------------------------------------------
1 | package realtime
2 |
3 | // MessageSendFunc is a function that sends a message over the network.
4 | type MessageSendFunc func(message []byte) error
5 |
6 | // Client is a subscriber one or multiple channels.
7 | type Client struct {
8 | Id string
9 | sendMessage MessageSendFunc
10 | properties map[string]interface{}
11 | }
12 |
13 | // NewClient creates a Client
14 | func NewClient(sendMessage MessageSendFunc, properties map[string]interface{}) *Client {
15 | return &Client{
16 | Id: "",
17 | sendMessage: sendMessage,
18 | properties: properties,
19 | }
20 | }
21 |
22 | // Send sends the message using the client's MessageSendFunc.
23 | func (client *Client) Send(message []byte) error {
24 | return client.sendMessage(message)
25 | }
26 |
27 | // Get returns a property from the client's properties or (any, false)
28 | func (client *Client) Get(key string) (value interface{}, exists bool) {
29 | if val, ok := client.properties[key]; ok {
30 | return val, ok
31 | }
32 | return nil, false
33 | }
34 |
35 | // Set sets a property on the client's properties.
36 | func (client *Client) Set(key string, value interface{}) {
37 | client.properties[key] = value
38 | }
39 |
--------------------------------------------------------------------------------
/tools/realtime/connector/melody.go:
--------------------------------------------------------------------------------
1 | package connector
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gabstv/melody"
7 |
8 | "github.com/TUM-Dev/gocast/tools/realtime"
9 | )
10 |
11 | // NewMelodyConnector creates a *realtime.Connector with Melody as underlying transport
12 | func NewMelodyConnector() *realtime.Connector {
13 | melodyInstance := melody.New()
14 | melodyInstance.Config.MaxMessageSize = 1200
15 | // 1200 bytes allow a little more than 1000 chars.
16 | connector := realtime.NewConnector(
17 | func(writer http.ResponseWriter, request *http.Request, properties map[string]interface{}) error {
18 | return melodyInstance.HandleRequestWithKeys(writer, request, properties)
19 | },
20 | )
21 |
22 | melodyInstance.HandleConnect(func(s *melody.Session) {
23 | client := connector.Join(
24 | func(message []byte) error {
25 | return s.Write(message)
26 | },
27 | s.Keys(),
28 | )
29 | s.Set("id", client.Id)
30 | })
31 |
32 | melodyInstance.HandleDisconnect(func(s *melody.Session) {
33 | id, _ := s.Get("id")
34 | connector.Leave(id.(string))
35 | })
36 |
37 | melodyInstance.HandleMessage(func(s *melody.Session, data []byte) {
38 | id, _ := s.Get("id")
39 | connector.Message(id.(string), data)
40 | })
41 |
42 | return connector
43 | }
44 |
--------------------------------------------------------------------------------
/tools/realtime/realtime_logger.go:
--------------------------------------------------------------------------------
1 | package realtime
2 |
3 | import (
4 | "log/slog"
5 | "os"
6 | )
7 |
8 | var logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
9 | Level: slog.LevelDebug,
10 | })).With("service", "realtime")
11 |
--------------------------------------------------------------------------------
/tools/session.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/golang-jwt/jwt/v5"
8 | )
9 |
10 | type SessionData struct {
11 | Userid uint
12 | SamlSubjectID *string
13 | }
14 |
15 | func StartSession(c *gin.Context, data *SessionData) {
16 | token, err := createToken(data.Userid, data.SamlSubjectID)
17 | if err != nil {
18 | logger.Error("Could not create token", "err", err)
19 | return
20 | }
21 | c.SetCookie("jwt", token, 60*60*24*7, "/", "", CookieSecure, true)
22 | }
23 |
24 | func createToken(user uint, samlSubjectID *string) (string, error) {
25 | t := jwt.New(jwt.GetSigningMethod("RS256"))
26 |
27 | t.Claims = &JWTClaims{
28 | RegisteredClaims: &jwt.RegisteredClaims{
29 | ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(time.Hour * 24 * 7)}, // Token expires in one week
30 | },
31 | UserID: user,
32 | SamlSubjectID: samlSubjectID,
33 | }
34 | return t.SignedString(Cfg.GetJWTKey())
35 | }
36 |
--------------------------------------------------------------------------------
/tools/template_executor.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "html/template"
5 | "io"
6 |
7 | "github.com/Masterminds/sprig/v3"
8 | )
9 |
10 | type TemplateExecutor interface {
11 | ExecuteTemplate(w io.Writer, name string, data interface{}) error
12 | }
13 |
14 | type DebugTemplateExecutor struct {
15 | Patterns []string
16 | }
17 |
18 | func (e DebugTemplateExecutor) ExecuteTemplate(w io.Writer, name string, data interface{}) error {
19 | if len(e.Patterns) == 0 {
20 | panic("Provide at least one pattern for the debug template executor.")
21 | }
22 |
23 | t, err := template.New("base").Funcs(sprig.FuncMap()).ParseGlob(e.Patterns[0])
24 | if err != nil {
25 | logger.Error("Failed to load pattern: '"+e.Patterns[0], "err", err.Error())
26 | }
27 |
28 | for i := 1; i < len(e.Patterns); i++ {
29 | pattern := e.Patterns[i]
30 | _, err := t.ParseGlob(pattern)
31 | if err != nil {
32 | logger.Error("Failed to load pattern: '"+pattern+"'.", "err", err.Error())
33 | }
34 | }
35 |
36 | return t.ExecuteTemplate(w, name, data)
37 | }
38 |
39 | type ReleaseTemplateExecutor struct {
40 | Template *template.Template
41 | }
42 |
43 | func (e ReleaseTemplateExecutor) ExecuteTemplate(w io.Writer, name string, data interface{}) error {
44 | return e.Template.ExecuteTemplate(w, name, data)
45 | }
46 |
--------------------------------------------------------------------------------
/tools/testutils/testutils.go:
--------------------------------------------------------------------------------
1 | package testutils
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/TUM-Dev/gocast/tools"
7 | "github.com/gin-gonic/gin"
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func Equal(t *testing.T, a, b interface{}) {
12 | assert.Equal(t, a, b)
13 | }
14 |
15 | func GetMiddlewares(mw ...func(ctx *gin.Context)) []func(c *gin.Context) {
16 | return mw
17 | }
18 |
19 | func TUMLiveContext(ctx tools.TUMLiveContext) func(c *gin.Context) {
20 | return func(c *gin.Context) {
21 | c.Set("TUMLiveContext", ctx)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tools/timing/time.go:
--------------------------------------------------------------------------------
1 | // Package timing provides time calculation functions used in TUM Live
2 | package timing
3 |
4 | import (
5 | "time"
6 |
7 | "github.com/jinzhu/now"
8 | )
9 |
10 | // GetWeeksInYear returns the number of weeks in the given year
11 | func GetWeeksInYear(year int) int {
12 | now.WeekStartDay = time.Monday
13 | yearStart := time.Date(year, 1, 1, 0, 0, 0, 0, time.Local)
14 | endOfYear := now.New(yearStart).EndOfYear()
15 | firstDayOfLastWeek := now.New(endOfYear).BeginningOfWeek()
16 | y, w := firstDayOfLastWeek.ISOWeek()
17 | for y != year {
18 | firstDayOfLastWeek = firstDayOfLastWeek.Add(time.Hour * -24)
19 | y, w = firstDayOfLastWeek.ISOWeek()
20 | }
21 | return w
22 | }
23 |
--------------------------------------------------------------------------------
/tools/timing/time_test.go:
--------------------------------------------------------------------------------
1 | package timing
2 |
3 | import "testing"
4 |
5 | func TestGetWeeksInYear(t *testing.T) {
6 | // example values from https://www.epochconverter.com/weeks/2020
7 | yw := map[int]int{2020: 53, 2021: 52, 2022: 52, 2023: 52, 2024: 52, 2025: 52, 2026: 53}
8 | for y, w := range yw {
9 | if wiy := GetWeeksInYear(y); wiy != w {
10 | t.Errorf("GetWeeksInYear(%d) = %d, want %d", y, wiy, w)
11 | }
12 | }
13 | for i := 0; i < 2020; i++ {
14 | if wiy := GetWeeksInYear(i); wiy > 53 || wiy < 52 {
15 | t.Errorf("GetWeeksInYear(%d) = %d, but must be either 52 or 53", i, wiy)
16 | }
17 | }
18 | }
19 |
20 | func BenchmarkGetWeeksInYear(b *testing.B) {
21 | for i := 0; i < b.N; i++ {
22 | _ = GetWeeksInYear(i)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tools/tools_logger.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "log/slog"
5 | "os"
6 | )
7 |
8 | var logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
9 | Level: slog.LevelDebug,
10 | })).With("service", "tools")
11 |
--------------------------------------------------------------------------------
/tools/tum/tum_logger.go:
--------------------------------------------------------------------------------
1 | package tum
2 |
3 | import (
4 | "log/slog"
5 | "os"
6 | )
7 |
8 | var logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
9 | Level: slog.LevelDebug,
10 | })).With("service", "tum")
11 |
--------------------------------------------------------------------------------
/tumlive.example.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=TUM-Live
3 | After=network.target
4 | Requires=mariadb.service
5 |
6 | [Service]
7 | LimitNOFILE=1048576:2097152
8 | Type=simple
9 | ExecStart=/bin/tum-live
10 | TimeoutStopSec=5
11 | KillMode=mixed
12 | Restart=on-failure
13 | StandardOutput=append:/var/log/tum-live/logs.log
14 | StandardError=append:/var/log/tum-live/error.log
15 |
16 | [Install]
17 | WantedBy=multi-user.target
18 |
--------------------------------------------------------------------------------
/vod-service/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.21-alpine3.18 as builder
2 |
3 | WORKDIR /app
4 |
5 | COPY . .
6 |
7 | RUN go build -o /vod-service cmd/vod-service/main.go
8 |
9 | FROM alpine:3.18
10 |
11 | RUN apk add ffmpeg
12 | COPY --from=builder /vod-service /vod-service
13 |
14 | EXPOSE 8089
15 |
16 | CMD ["/vod-service"]
--------------------------------------------------------------------------------
/vod-service/cmd/vod-service/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/TUM-Dev/gocast/vod-service/internal"
4 |
5 | func main() {
6 | app := internal.NewApp()
7 | app.Run()
8 | }
9 |
--------------------------------------------------------------------------------
/vod-service/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/TUM-Dev/gocast/vod-service
2 |
3 | go 1.19
4 |
--------------------------------------------------------------------------------
/vod-service/internal/internal_logger.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "log/slog"
5 | "os"
6 | )
7 |
8 | var logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
9 | Level: slog.LevelDebug,
10 | })).With("service", "internal_vod_service")
11 |
--------------------------------------------------------------------------------
/voice-service/subtitles.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 | package live.voice.v1;
3 | option go_package = "voice-service/pb";
4 |
5 | import "google/protobuf/empty.proto";
6 |
7 | // Implemented in voice-service
8 | service SubtitleGenerator {
9 | rpc Generate (GenerateRequest) returns (google.protobuf.Empty) {}
10 | }
11 |
12 | // Implemented in tum-live
13 | service SubtitleReceiver {
14 | rpc Receive (ReceiveRequest) returns (google.protobuf.Empty) {}
15 | }
16 |
17 | message ReceiveRequest {
18 | int32 stream_id = 1;
19 | string subtitles = 2;
20 | string language = 3;
21 | }
22 |
23 | message GenerateRequest {
24 | int32 stream_id = 1;
25 | string source_file = 2;
26 | string language = 3;
27 | }
--------------------------------------------------------------------------------
/web/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "plugins": [
5 | "@typescript-eslint"
6 | ],
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:@typescript-eslint/eslint-recommended",
10 | "plugin:@typescript-eslint/recommended",
11 | "plugin:prettier/recommended"
12 | ],
13 | "rules": {
14 | "@typescript-eslint/ban-ts-ignore": "off",
15 | "@typescript-eslint/ban-ts-comment": "off",
16 | "@typescript-eslint/no-unused-vars": "off"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/web/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | semi: true,
3 | trailingComma: "all",
4 | printWidth: 120,
5 | tabWidth: 4,
6 | bracketSpacing: true,
7 | endOfLine: "auto",
8 | };
9 |
--------------------------------------------------------------------------------
/web/assets/css/watch.css:
--------------------------------------------------------------------------------
1 | .playlist-thumbnail {
2 | @apply relative bg-gray-100 dark:bg-gray-800 rounded-lg mr-4 bg-cover transition-all duration-500 hover:shadow-xl dark:shadow-gray-900/75;
3 | }
4 |
5 | /* Rendered, but not visible on the screen. Enables layout calculations. */
6 | .offscreen {
7 | position: absolute;
8 | left: -9999px;
9 | top: -9999px;
10 | width: 100%;
11 | height: auto;
12 | overflow: visible;
13 | }
14 |
15 | .pulsing-circle {
16 | width: 2ch;
17 | height: 2ch;
18 | border-radius: 50%;
19 | background-color: red;
20 | animation: pulse 2s infinite;
21 | margin-left: .2ch;
22 | }
23 |
24 | @keyframes pulse {
25 | 0% {
26 | opacity: 1;
27 | transform: scale(1);
28 | }
29 | 50% {
30 | opacity: 0.5;
31 | transform: scale(1.2);
32 | }
33 | 100% {
34 | opacity: 1;
35 | transform: scale(1);
36 | }
37 | }
--------------------------------------------------------------------------------
/web/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/web/assets/favicon.ico
--------------------------------------------------------------------------------
/web/assets/img/icons-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/web/assets/img/icons-192.png
--------------------------------------------------------------------------------
/web/assets/img/icons-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/web/assets/img/icons-512.png
--------------------------------------------------------------------------------
/web/assets/img/thumb-fallback.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/web/assets/img/thumb-fallback.png
--------------------------------------------------------------------------------
/web/assets/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "TUM Live",
3 | "short_name": "TUM Live",
4 | "icons": [
5 | {
6 | "src": "/static/assets/img/icons-192.png",
7 | "type": "image/png",
8 | "sizes": "192x192"
9 | },
10 | {
11 | "src": "/static/assets/img/icons-512.png",
12 | "type": "image/png",
13 | "sizes": "512x512"
14 | }
15 | ],
16 | "start_url": "/?source=pwa",
17 | "background_color": "#161c22",
18 | "display_override": ["window-control-overlay", "minimal-ui"],
19 | "display": "standalone",
20 | "scope": "/",
21 | "theme_color": "#161c22",
22 | "description": "TUM-Live, the livestreaming and VoD service of the Rechnerbetriebsgruppe at the department of informatics and mathematics at the Technical University of Munich",
23 | "screenshots": []
24 | }
--------------------------------------------------------------------------------
/web/popup.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 |
7 | "github.com/TUM-Dev/gocast/tools"
8 | "github.com/getsentry/sentry-go"
9 | "github.com/gin-gonic/gin"
10 | )
11 |
12 | func (r mainRoutes) PopOutChat(c *gin.Context) {
13 | foundContext, exists := c.Get("TUMLiveContext")
14 | if !exists {
15 | sentry.CaptureException(errors.New("context should exist but doesn't"))
16 | c.AbortWithStatus(http.StatusInternalServerError)
17 | return
18 | }
19 |
20 | var data ChatData
21 |
22 | tumLiveContext := foundContext.(tools.TUMLiveContext)
23 | data.IndexData = NewIndexData()
24 | data.IndexData.TUMLiveContext = foundContext.(tools.TUMLiveContext)
25 | data.IsAdminOfCourse = tumLiveContext.UserIsAdmin()
26 |
27 | err := templateExecutor.ExecuteTemplate(c.Writer, "popup-chat.gohtml", data)
28 | if err != nil {
29 | logger.Error("couldn't render template popup-chat.gohtml", "err", err)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/web/template/error.gohtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{.Branding.Title}} | {{.Status}}
6 | {{template "headImports"}}
7 |
8 |
31 |
32 |
33 | {{- /*gotype: github.com/TUM-Dev/gocast/tools.ErrorPageData*/ -}}
34 | {{template "header"}}
35 |
36 |
37 |
38 |
Error: {{.Status}}
39 |
{{.Message}}
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/web/template/info-page.gohtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{.Branding.Title}}
6 |
7 |
8 |
10 |
13 |
14 | {{- /*gotype: github.com/TUM-Dev/gocast/web.IndexData*/ -}}
15 |
16 |
23 |
24 |
25 | {{.Text}}
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/web/template/partial/close-btn.gohtml:
--------------------------------------------------------------------------------
1 | {{define "close-button"}}
2 |
10 | {{end}}
--------------------------------------------------------------------------------
/web/template/partial/course/manage/camera-presets.gohtml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/web/template/partial/course/manage/camera-presets.gohtml
--------------------------------------------------------------------------------
/web/template/partial/footer.gohtml:
--------------------------------------------------------------------------------
1 | {{define "footer"}}
2 | {{- /*gotype: github.com/TUM-Dev/gocast/web.IndexData*/ -}}
3 |
15 | {{end}}
16 |
17 | {{define "mobile_footer"}}
18 | {{- /*gotype: github.com/TUM-Dev/gocast/web.IndexData*/ -}}
19 |
29 | {{end}}
--------------------------------------------------------------------------------
/web/template/partial/terminalprompt.gohtml:
--------------------------------------------------------------------------------
1 | {{define "terminalprompt"}}
2 |
7 | {{end}}
--------------------------------------------------------------------------------
/web/template/popup-chat.gohtml:
--------------------------------------------------------------------------------
1 | {{- /*gotype: github.com/TUM-Dev/gocast/web.ChatData*/ -}}
2 | {{$course := .IndexData.TUMLiveContext.Course}}
3 | {{$stream := .IndexData.TUMLiveContext.Stream}}
4 |
5 |
6 |
7 |
8 | {{.IndexData.Branding.Title}} | {{$course.Name}}: {{$stream.Name}}
9 | {{template "headImports" .IndexData.VersionTag}}
10 |
11 | {{if $stream.ChatEnabled}}
12 |
13 |
14 |
15 |
16 | {{end}}
17 |
19 |
20 |
21 | {{template "chat-component" .}}
22 |
23 |
--------------------------------------------------------------------------------
/web/template/reload-page-button.gohtml:
--------------------------------------------------------------------------------
1 | {{define "reloadpagebutton"}}
2 |
8 | Something has changed on this page
9 |
10 |
13 |
16 |
17 |
18 | {{end}}
--------------------------------------------------------------------------------
/web/ts/api/Identifiable.ts:
--------------------------------------------------------------------------------
1 | interface Identifiable {
2 | ID: number;
3 | }
4 |
--------------------------------------------------------------------------------
/web/ts/api/notifications.ts:
--------------------------------------------------------------------------------
1 | import { get } from "../utilities/fetch-wrappers";
2 |
3 | /**
4 | * REST API Wrapper for /api/notifications
5 | */
6 | export const NotificationAPI = {
7 | async getServerNotifications() {
8 | return get("/api/notifications/server");
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/web/ts/api/poll-ws.ts:
--------------------------------------------------------------------------------
1 | import { RealtimeFacade } from "../utilities/ws";
2 | import { Realtime } from "../socket";
3 |
4 | export enum PollMessageType {
5 | StartPoll = "start_poll",
6 | SubmitPollOptionVote = "submit_poll_option_vote",
7 | CloseActivePoll = "close_active_poll",
8 | }
9 |
10 | export class PollWebsocketConnection {
11 | private readonly ws: RealtimeFacade;
12 |
13 | constructor(ws: RealtimeFacade) {
14 | this.ws = ws;
15 | }
16 |
17 | startPoll(question: string, pollAnswers: string[]) {
18 | return this.ws.send({
19 | payload: {
20 | type: PollMessageType.StartPoll,
21 | question,
22 | pollAnswers,
23 | },
24 | });
25 | }
26 |
27 | submitPollOptionVote(pollOptionId: number) {
28 | return this.ws.send({
29 | payload: {
30 | type: PollMessageType.SubmitPollOptionVote,
31 | pollOptionId,
32 | },
33 | });
34 | }
35 |
36 | closeActivePoll() {
37 | return this.ws.send({ payload: { type: PollMessageType.CloseActivePoll } });
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/web/ts/api/semesters.ts:
--------------------------------------------------------------------------------
1 | import { get } from "../utilities/fetch-wrappers";
2 |
3 | export type SemesterDTO = {
4 | Current: Semester;
5 | Semesters: Semester[];
6 | };
7 |
8 | export class Semester {
9 | TeachingTerm: string;
10 | Year: number;
11 |
12 | constructor(obj: Semester) {
13 | this.TeachingTerm = obj.TeachingTerm;
14 | this.Year = obj.Year;
15 | }
16 |
17 | public FriendlyString(): string {
18 | if (this.TeachingTerm === "W") return `Winter ${this.Year}/` + `${this.Year + 1}`.slice(-2);
19 | else return `Summer ${this.Year}`;
20 | }
21 | }
22 |
23 | /**
24 | * REST API Wrapper for /api/semesters
25 | */
26 | export const SemestersAPI = {
27 | async get(): Promise {
28 | return get("/api/semesters").then((l: SemesterDTO) => {
29 | l.Semesters = l.Semesters.map((s) => new Semester(s));
30 | return l;
31 | });
32 | },
33 | };
34 |
--------------------------------------------------------------------------------
/web/ts/api/users.ts:
--------------------------------------------------------------------------------
1 | import { get, post } from "../utilities/fetch-wrappers";
2 |
3 | export class User implements Identifiable {
4 | ID: number;
5 | name: string;
6 | isAdmin: boolean;
7 | }
8 |
9 | export type HasPinnedCourseDTO = {
10 | has: boolean;
11 | };
12 |
13 | /**
14 | * REST API Wrapper for /api/users
15 | */
16 | export const UserAPI = {
17 | async hasPinnedCourse(courseID: number) {
18 | return get(`/api/users/courses/${courseID}/pin`, { has: false });
19 | },
20 |
21 | async pinCourse(courseID: number) {
22 | return post("/api/users/courses/pin", { courseID });
23 | },
24 |
25 | async unpinCourse(courseID: number) {
26 | return post("/api/users/courses/unpin", { courseID });
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/web/ts/api/video-sections.ts:
--------------------------------------------------------------------------------
1 | import { del, get, post, put } from "../utilities/fetch-wrappers";
2 |
3 | export type Section = {
4 | ID?: number;
5 | description: string;
6 |
7 | startHours: number;
8 | startMinutes: number;
9 | startSeconds: number;
10 |
11 | streamID: number;
12 | friendlyTimestamp?: string;
13 | fileID?: number;
14 |
15 | isCurrent: boolean;
16 | };
17 |
18 | /**
19 | * REST API Wrapper for /api/stream/:id/sections
20 | */
21 | export const VideoSectionAPI = {
22 | get: async function (streamId: number): Promise {
23 | return get(`/api/stream/${streamId}/sections`);
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/web/ts/api/watched.ts:
--------------------------------------------------------------------------------
1 | import { post } from "../utilities/fetch-wrappers";
2 |
3 | export const WatchedAPI = {
4 | async update(streamID: number, watched: boolean) {
5 | return post("/api/watched", { streamID, watched });
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/web/ts/audits.ts:
--------------------------------------------------------------------------------
1 | export class audit {
2 | id: number;
3 | createdAt: string;
4 | message: string;
5 | type: string;
6 | userID: number;
7 | userName: string;
8 | }
9 |
10 | export function audits(offset: number, limit: number): Promise {
11 | return fetch(`/api/audits?offset=${offset}&limit=${limit}`).then((r) => {
12 | return r.json() as Promise;
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/web/ts/chat/ChatMessageSorter.ts:
--------------------------------------------------------------------------------
1 | import { ChatMessage } from "../api/chat";
2 |
3 | export enum ChatSortMode {
4 | LiveChat,
5 | PopularFirst,
6 | }
7 |
8 | type CompareFn = (a: ChatMessage, b: ChatMessage) => number;
9 |
10 | export abstract class ChatMessageSorter {
11 | static GetSortFn(sortMode: ChatSortMode): CompareFn {
12 | switch (sortMode) {
13 | case ChatSortMode.LiveChat:
14 | return (a: ChatMessage, b: ChatMessage) => (a.ID < b.ID ? -1 : 1);
15 | case ChatSortMode.PopularFirst:
16 | return (a: ChatMessage, b: ChatMessage) => {
17 | const likesA = a.getLikes();
18 | const likesB = b.getLikes();
19 |
20 | if (likesA === likesB) {
21 | return a.ID - b.ID; // same amount of likes -> newer messages up
22 | }
23 | return likesB - likesA; // more likes -> up
24 | };
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/web/ts/chat/misc.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * get currently typed word based on position in the input.
3 | * e.g.: "hello{cursor} world" => "hello" ([0, 4])
4 | */
5 | export function getCurrentWordPositions(input: string, cursorPos: number): [number, number] {
6 | const cursorStart = cursorPos;
7 | while (cursorPos > 0 && input.charAt(cursorPos - 1) !== " ") {
8 | cursorPos--;
9 | }
10 | return [cursorPos, cursorStart];
11 | }
12 |
--------------------------------------------------------------------------------
/web/ts/components/alpine-component.ts:
--------------------------------------------------------------------------------
1 | export abstract class AlpineComponent {
2 | abstract init(): void;
3 | }
4 |
--------------------------------------------------------------------------------
/web/ts/components/emoji-picker.ts:
--------------------------------------------------------------------------------
1 | import { AlpineComponent } from "./alpine-component";
2 | import { TopEmojis } from "top-twitter-emojis-map";
3 |
4 | export function emojiPickerContext(id: number): AlpineComponent {
5 | return {
6 | id: id,
7 |
8 | emojiSuggestions: ["👍", "👎", "😄", "🎉", "😕", "❤️", "👀"].map((e) =>
9 | TopEmojis.find(({ emoji }) => emoji === e),
10 | ),
11 |
12 | // eslint-disable-next-line @typescript-eslint/no-empty-function
13 | init() {},
14 | } as AlpineComponent;
15 | }
16 |
--------------------------------------------------------------------------------
/web/ts/components/header.ts:
--------------------------------------------------------------------------------
1 | import { Notifications } from "../notifications";
2 | import { ToggleableElement } from "../utilities/ToggleableElement";
3 |
4 | export function header() {
5 | return {
6 | userContext: new ToggleableElement([["themePicker", new ToggleableElement()]]),
7 |
8 | notifications: new Notifications(),
9 | notification: new ToggleableElement(),
10 | toggleNotification(set?: boolean) {
11 | this.notification.toggle(set);
12 | this.notifications.writeToStorage(true);
13 | },
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/web/ts/components/livestreams.ts:
--------------------------------------------------------------------------------
1 | import { CoursesAPI, Livestream } from "../api/courses";
2 | import { AlpineComponent } from "./alpine-component";
3 |
4 | export function livestreams(predicate?: (s: Livestream) => boolean): AlpineComponent {
5 | return {
6 | _all: [] as Livestream[],
7 |
8 | livestreams: [] as Livestream[],
9 | init() {
10 | this.reload();
11 | },
12 |
13 | async reload() {
14 | this._all = await CoursesAPI.getLivestreams();
15 | this.livestreams = predicate ? this._all.filter(predicate) : this._all;
16 | console.log("🌑 init livestreams", this.livestreams);
17 | },
18 |
19 | refilter(predicate?: (s: Livestream) => boolean) {
20 | this.livestreams = predicate ? this._all.filter(predicate) : this._all;
21 | },
22 |
23 | hasLivestreams() {
24 | return this.livestreams.length > 0;
25 | },
26 | } as AlpineComponent;
27 | }
28 |
--------------------------------------------------------------------------------
/web/ts/components/popup.ts:
--------------------------------------------------------------------------------
1 | import { AlpineComponent } from "./alpine-component";
2 | import { RealtimeFacade } from "../utilities/ws";
3 | import { SocketConnections } from "../api/chat-ws";
4 |
5 | export function popupContext(streamId: number): AlpineComponent {
6 | return {
7 | init() {
8 | // subscription?
9 | SocketConnections.ws = new RealtimeFacade("chat/" + streamId);
10 | // ws needs to subscribe, so that pop-out chat can work
11 | const handler = (data) => {};
12 | SocketConnections.ws.subscribe(handler);
13 | },
14 | } as AlpineComponent;
15 | }
16 |
17 | export function closeChatOnEscapePressed() {
18 | document.addEventListener("keyup", function (event) {
19 | if (event.key === "Escape") {
20 | window.close();
21 | }
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/web/ts/components/servernotifications.ts:
--------------------------------------------------------------------------------
1 | import { NotificationAPI } from "../api/notifications";
2 |
3 | export function serverNotifications() {
4 | return {
5 | serverNotifications: [],
6 | init() {
7 | this.load();
8 | },
9 |
10 | async load() {
11 | this.serverNotifications = await NotificationAPI.getServerNotifications();
12 | },
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/web/ts/components/video-interaction.ts:
--------------------------------------------------------------------------------
1 | import { AlpineComponent } from "./alpine-component";
2 | import { User } from "../api/users";
3 | import { SocketConnections } from "../api/chat-ws";
4 |
5 | enum InteractionType {
6 | Chat,
7 | Polls,
8 | }
9 |
10 | export function videoInteractionContext(user: User) {
11 | return {
12 | type: InteractionType.Chat,
13 | user: user as User,
14 |
15 | // eslint-disable-next-line @typescript-eslint/no-empty-function
16 | init() {},
17 |
18 | showChat() {
19 | this.type = InteractionType.Chat;
20 | },
21 |
22 | isChat(): boolean {
23 | return this.type === InteractionType.Chat;
24 | },
25 |
26 | showPolls() {
27 | this.type = InteractionType.Polls;
28 | },
29 |
30 | isPolls(): boolean {
31 | return this.type === InteractionType.Polls;
32 | },
33 |
34 | isLoggedIn(): boolean {
35 | return this.user.ID !== 0;
36 | },
37 |
38 | isAdmin(): boolean {
39 | return this.user.isAdmin;
40 | },
41 |
42 | isPopOut(): boolean {
43 | return window.location.href.includes("/chat/popup");
44 | },
45 | } as AlpineComponent;
46 | }
47 |
--------------------------------------------------------------------------------
/web/ts/custom-elements/elements.ts:
--------------------------------------------------------------------------------
1 | import * as help from "./help-icon";
2 |
3 | export function defineElements() {
4 | customElements.define("help-icon", help.HelpIcon);
5 | console.log("Defined custom elements");
6 | }
7 |
--------------------------------------------------------------------------------
/web/ts/custom-elements/help-icon.ts:
--------------------------------------------------------------------------------
1 | export class HelpIcon extends HTMLElement {
2 | private text: string;
3 | constructor() {
4 | super();
5 | }
6 |
7 | connectedCallback() {
8 | this.text = this.getAttribute("text") ?? "No help available";
9 | this.innerHTML = `
10 |
15 |
20 |
21 |
24 | ${this.text}
25 |
26 |
27 | `;
28 | this.className = "m-0 p-0 text-xs";
29 | this.style.textRendering = "";
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/web/ts/data-store/data-store.ts:
--------------------------------------------------------------------------------
1 | import { VideoSectionProvider } from "./video-sections";
2 | import { BookmarksProvider } from "./bookmarks";
3 | import { StreamPlaylistProvider } from "./stream-playlist";
4 | import { AdminLectureListProvider } from "./admin-lecture-list";
5 |
6 | export abstract class DataStore {
7 | static bookmarks: BookmarksProvider = new BookmarksProvider();
8 | static videoSections: VideoSectionProvider = new VideoSectionProvider();
9 | static streamPlaylist: StreamPlaylistProvider = new StreamPlaylistProvider();
10 |
11 | // Admin Data-Stores
12 | static adminLectureList: AdminLectureListProvider = new AdminLectureListProvider();
13 | }
14 |
--------------------------------------------------------------------------------
/web/ts/data-store/video-sections.ts:
--------------------------------------------------------------------------------
1 | import { Time } from "../global";
2 | import { StreamableMapProvider } from "./provider";
3 | import { Section, VideoSectionAPI } from "../api/video-sections";
4 |
5 | export class VideoSectionProvider extends StreamableMapProvider {
6 | protected async fetcher(streamId: number): Promise {
7 | const result = await VideoSectionAPI.get(streamId);
8 | return result.map((s) => {
9 | s.friendlyTimestamp = new Time(s.startHours, s.startMinutes, s.startSeconds).toString();
10 | return s;
11 | });
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/web/ts/entry/admins.ts:
--------------------------------------------------------------------------------
1 | /* This bundle contains all functionality that is needed for admins, besides video.js dependencies */
2 | export * from "../course-import";
3 | export * from "../schedule";
4 | export * from "../stats";
5 | export * from "../edit-course";
6 | export * from "../create-course";
7 | export * from "../onboarding";
8 | export * from "../lecture-hall-management";
9 | export * from "../admin";
10 | export * from "../token-management";
11 | export * from "../worker";
12 | export * from "../courseAdminManagement";
13 | export * from "../notification-management";
14 | export * from "../audits";
15 | export * from "../maintenance";
16 | export * from "../change-set";
17 | export * from "../api/runner";
18 |
--------------------------------------------------------------------------------
/web/ts/entry/home.ts:
--------------------------------------------------------------------------------
1 | export * from "../views/home";
2 | export * from "../components/header";
3 | export * from "../components/livestreams";
4 | export * from "../components/course";
5 | export * from "../components/servernotifications";
6 | export * from "../components/main";
7 | export * from "../search";
8 | export * from "../utilities/date";
9 | export * from "../utilities/lectureHallValidator";
10 |
--------------------------------------------------------------------------------
/web/ts/entry/interactions.ts:
--------------------------------------------------------------------------------
1 | export * from "../components/video-interaction";
2 | export * from "../components/chat";
3 | export * from "../components/chat-prompt";
4 | export * from "../components/poll";
5 | export * from "../components/emoji-picker";
6 | export * from "../components/video-information";
7 | export * from "../components/popup";
8 |
--------------------------------------------------------------------------------
/web/ts/entry/user.ts:
--------------------------------------------------------------------------------
1 | /* This bundle contains all functionality that students depend on when not watching a video */
2 | export * from "../global";
3 | export * from "../course-overview";
4 |
--------------------------------------------------------------------------------
/web/ts/entry/video.ts:
--------------------------------------------------------------------------------
1 | /* This bundle contains everything regarding the watch page */
2 | export * from "../watch-admin";
3 | export * from "../TUMLiveVjs";
4 | export * from "../watch";
5 | export * from "../splitview";
6 | export * from "../bookmarks";
7 | export * from "../transcript";
8 | export * from "../subtitle-search";
9 | export * from "../components/video-sections";
10 | // Lecture Units are currently not used, so we don't include them in the bundle at the moment
11 | export * from "../interval-updates";
12 |
--------------------------------------------------------------------------------
/web/ts/interval-updates.ts:
--------------------------------------------------------------------------------
1 | export function periodicCurrentTime(id: string) {
2 | const time = document.getElementById(id);
3 | setInterval(() => (time.innerHTML = new Date().toLocaleTimeString()), 1000);
4 | }
5 |
--------------------------------------------------------------------------------
/web/ts/notification-management.ts:
--------------------------------------------------------------------------------
1 | import { Notification } from "./notifications";
2 |
3 | export function createNotification(body: string, target: number, title: string | undefined = undefined): void {
4 | const notification = new Notification(title, body, target);
5 | fetch("/api/notifications/", {
6 | method: "POST",
7 | headers: {
8 | "Content-Type": "application/json",
9 | },
10 | body: JSON.stringify({
11 | title: notification.title,
12 | body: notification.body,
13 | target: Number(notification.target),
14 | }),
15 | })
16 | .then((r) => r.json())
17 | .then((r) => {
18 | window.location.reload();
19 | });
20 | }
21 |
22 | export function deleteNotification(id: number): void {
23 | console.log("Deleting notification with id: " + id);
24 | fetch("/api/notifications/" + id, {
25 | method: "DELETE",
26 | })
27 | .then((r) => r.json())
28 | .then((r) => {
29 | window.location.reload();
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/web/ts/start-page.ts:
--------------------------------------------------------------------------------
1 | import { Realtime } from "./socket";
2 |
3 | export const liveUpdateListener = {
4 | async init() {
5 | await Realtime.get().subscribeChannel("live-update", this.handle);
6 | },
7 |
8 | handle(payload: object) {
9 | window.dispatchEvent(new CustomEvent("liveupdate", { detail: { data: payload } }));
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/web/ts/subtitle-search.ts:
--------------------------------------------------------------------------------
1 | export function subtitleSearch(streamID: number) {
2 | return {
3 | hits: [],
4 | open: false,
5 | lastEventTimestamp: 0,
6 | search: function (query: string) {
7 | if (query.length > 2) {
8 | fetch(`/api/search/stream/${streamID}/subtitles?q=${query}`).then((res) => {
9 | if (res.ok) {
10 | res.json().then((data) => {
11 | this.hits = data.hits;
12 | this.open = this.hits.length > 0;
13 | });
14 | }
15 | });
16 | } else {
17 | this.hits = [];
18 | this.open = false;
19 | }
20 | },
21 | openRes: function () {
22 | if (this.lastEventTimestamp + 1000 < Date.now()) {
23 | this.lastEventTimestamp = Date.now();
24 | this.open = true;
25 | }
26 | },
27 | closeRes: function () {
28 | if (this.lastEventTimestamp + 1000 < Date.now()) {
29 | this.lastEventTimestamp = Date.now();
30 | this.open = false;
31 | }
32 | },
33 | };
34 | }
35 |
--------------------------------------------------------------------------------
/web/ts/token-management.ts:
--------------------------------------------------------------------------------
1 | import { postData } from "./global";
2 |
3 | export function createToken(expires: string, scope: string) {
4 | const req = {
5 | expires: null,
6 | scope: scope,
7 | };
8 | if (expires !== "") {
9 | const dateObj = new Date(expires);
10 | req.expires = dateObj.toISOString();
11 | }
12 | return postData("/api/token/create", req);
13 | }
14 |
15 | export function deleteToken(id: number) {
16 | return fetch(`/api/token/${id}`, {
17 | method: "DELETE",
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/web/ts/track-bars.ts:
--------------------------------------------------------------------------------
1 | import { VideoJsPlayer } from "video.js";
2 |
3 | const LANGUAGES = [
4 | { id: "en", label: "English" },
5 | { id: "de", label: "Deutsch" },
6 | ];
7 |
8 | export async function loadAndSetTrackbars(player: VideoJsPlayer, streamID: number) {
9 | for (const language of LANGUAGES) {
10 | await fetch(`/api/stream/${streamID}/subtitles/${language.id}`).then((res) => {
11 | if (res.ok) {
12 | window.dispatchEvent(new CustomEvent("togglesearch", { detail: { streamID: streamID } })); // Used for enabling watch searchbar
13 | window.dispatchEvent(new CustomEvent("toggletranscript", { detail: { streamID: streamID } })); // Used for enabling button to show transcript-modal
14 | player.addRemoteTextTrack(
15 | {
16 | src: `/api/stream/${streamID}/subtitles/${language.id}`,
17 | kind: "captions",
18 | label: language.label,
19 | },
20 | false,
21 | );
22 | }
23 | });
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/web/ts/utilities/ToggleableElement.ts:
--------------------------------------------------------------------------------
1 | export class ToggleableElement {
2 | private readonly children: Map;
3 |
4 | public value: boolean;
5 |
6 | constructor(children?: readonly [string, ToggleableElement][] | null, value = false) {
7 | this.children = children ? new Map(children) : new Map();
8 | this.value = value;
9 | }
10 |
11 | getChild(name: string): ToggleableElement {
12 | return this.children.get(name);
13 | }
14 |
15 | toggle(set?: boolean) {
16 | this.value = set ?? !this.value;
17 | if (!this.value) {
18 | this.children.forEach((c) => c.toggle(false));
19 | }
20 | }
21 |
22 | toggleText(a: string, b: string) {
23 | return this.value ? a : b;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/web/ts/utilities/date.ts:
--------------------------------------------------------------------------------
1 | export function datetimeToFriendly(time: string): string {
2 | const date = new Date(time);
3 | return date.toLocaleString();
4 | }
5 |
--------------------------------------------------------------------------------
/web/ts/utilities/input-interactions.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copies a string to the clipboard using clipboard API.
3 | * @param text the string that is copied to the clipboard.
4 | */
5 | export async function copyToClipboard(text: string): Promise {
6 | return navigator.clipboard.writeText(text).then(
7 | () => {
8 | return true;
9 | },
10 | () => {
11 | return false;
12 | },
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/web/ts/utilities/keycodes.ts:
--------------------------------------------------------------------------------
1 | export function isAlphaNumeric(keyCode: number) {
2 | return (keyCode >= 48 && keyCode <= 57) || (keyCode >= 65 && keyCode <= 90) || (keyCode >= 97 && keyCode <= 122);
3 | }
4 |
5 | export function isSpacebar(keyCode: number) {
6 | return keyCode == 32;
7 | }
8 |
--------------------------------------------------------------------------------
/web/ts/utilities/lectureHallValidator.ts:
--------------------------------------------------------------------------------
1 | export function isLectureHallValid(lectureHall: string): boolean {
2 | const regex = /^\d{4}\.[A-Z0-9]{2}\.[A-Z0-9]{3,4}$/;
3 | return regex.test(lectureHall);
4 | }
5 |
--------------------------------------------------------------------------------
/web/ts/utilities/sliding-window.ts:
--------------------------------------------------------------------------------
1 | import { Paginator } from "./paginator";
2 |
3 | export class SlidingWindow extends Paginator {
4 | constructor(list: T[], split_number: number) {
5 | super(list, split_number);
6 | }
7 |
8 | get(sortFn?: CompareFunction): T[] {
9 | const copy = [...this.list].filter(this.filterPred.bind(this));
10 | return sortFn
11 | ? copy.sort(sortFn).slice(0, this.index * this.split_number)
12 | : copy.slice(0, this.index * this.split_number);
13 | }
14 |
15 | isInWindow(o: T): boolean {
16 | const i = this.list.findIndex((o1) => o1 === o);
17 | return i !== -1 && this.filterPred(o, i);
18 | }
19 |
20 | show(o: T) {
21 | const i = this.list.findIndex((o1) => o1 === o);
22 | this.index = Math.floor(i / this.split_number) + 1;
23 | }
24 |
25 | prev() {
26 | this.index--;
27 | }
28 |
29 | hasPrev() {
30 | return this.index > 1;
31 | }
32 |
33 | private filterPred(o: T, index: number): boolean {
34 | return index >= (this.index - 1) * this.split_number && index < this.index * this.split_number;
35 | }
36 | }
37 |
38 | type CompareFunction = (a: T, b: T) => number;
39 |
--------------------------------------------------------------------------------
/web/ts/utilities/storage.ts:
--------------------------------------------------------------------------------
1 | export function setInStorage(key: string, value: string, storage = window.localStorage) {
2 | storage.setItem(key, value);
3 | }
4 |
5 | export function getFromStorage(key: string, storage = window.localStorage): string {
6 | return storage.getItem(key);
7 | }
8 |
--------------------------------------------------------------------------------
/web/ts/utilities/time-utils.ts:
--------------------------------------------------------------------------------
1 | export const same_day = (a: Date, b: Date) =>
2 | a.getDate() === b.getDate() && a.getMonth() == b.getMonth() && a.getFullYear() === b.getFullYear();
3 |
--------------------------------------------------------------------------------
/web/ts/utilities/tunnels.ts:
--------------------------------------------------------------------------------
1 | import { ValueStream } from "../value-stream";
2 | import { Course } from "../api/courses";
3 | import { ChatMessage } from "../api/chat";
4 |
5 | /**
6 | * Tunnels are an observer pattern based communication channel for multiple components
7 | */
8 | export abstract class Tunnel {
9 | static pinned: ValueStream = new ValueStream();
10 | static reply: ValueStream = new ValueStream();
11 | }
12 |
13 | export interface PinnedUpdate {
14 | pin: boolean /* true if pinned, false if unpinned */;
15 | course: Course;
16 | }
17 |
18 | export interface SetReply {
19 | message: ChatMessage;
20 | }
21 |
--------------------------------------------------------------------------------
/web/ts/utilities/url.ts:
--------------------------------------------------------------------------------
1 | export class CustomURL {
2 | private url: URL;
3 |
4 | constructor(url: string, searchParams?: object) {
5 | this.url = new URL(url, window.location.origin);
6 | if (searchParams) {
7 | for (const [key, value] of Object.entries(searchParams)) {
8 | if (value !== undefined) {
9 | this.url.searchParams.set(key, value);
10 | }
11 | }
12 | }
13 | }
14 |
15 | toString(): string {
16 | return this.url.toString();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/web/ts/utilities/ws.ts:
--------------------------------------------------------------------------------
1 | import { MessageHandlerFn, Realtime } from "../socket";
2 |
3 | export class RealtimeFacade {
4 | private readonly channel: string;
5 |
6 | constructor(channel: string) {
7 | this.channel = channel;
8 | }
9 |
10 | async subscribe(handler?: MessageHandlerFn) {
11 | Realtime.get().subscribeChannel(this.channel, handler);
12 | }
13 |
14 | async addHandler(handler: MessageHandlerFn) {
15 | Realtime.get().registerHandler(this.channel, handler);
16 | }
17 |
18 | send(payload: object = {}) {
19 | return Realtime.get().send(this.channel, payload);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/web/ts/value-stream.ts:
--------------------------------------------------------------------------------
1 | export class ValueStreamMap {
2 | protected streams: Map> = new Map>();
3 |
4 | private getStream(groupKey: string): ValueStream {
5 | if (this.streams[groupKey] == null) {
6 | this.streams[groupKey] = new ValueStream();
7 | }
8 | return this.streams[groupKey];
9 | }
10 |
11 | subscribe(groupKey: string, listener: ValueListener) {
12 | this.getStream(groupKey).subscribe(listener);
13 | }
14 |
15 | unsubscribe(groupKey: string, listener: ValueListener) {
16 | this.getStream(groupKey).unsubscribe(listener);
17 | }
18 |
19 | add(groupKey: string, data: T) {
20 | this.getStream(groupKey).add(data);
21 | }
22 | }
23 |
24 | export class ValueStream {
25 | protected listeners: ValueListener[] = [];
26 |
27 | subscribe(listener: ValueListener) {
28 | this.listeners.push(listener);
29 | }
30 |
31 | unsubscribe(listener: ValueListener) {
32 | this.listeners = this.listeners.filter((l) => l !== listener);
33 | }
34 |
35 | add(data: T) {
36 | for (const listener of this.listeners) {
37 | listener(data);
38 | }
39 | }
40 | }
41 |
42 | export type ValueListener = (value: T) => void;
43 |
--------------------------------------------------------------------------------
/web/ts/video/watchers.ts:
--------------------------------------------------------------------------------
1 | import { VideoJsPlayer } from "video.js";
2 |
3 | /**
4 | * Registers a time watcher that observes the time of the current player
5 | * @param player: The player to register the watcher.
6 | * @param callback call back function responsible for handling player time updates
7 | * @return callBack function that got registered for listening to player time updates (used to deregister)
8 | */
9 | export const registerTimeWatcher = function (
10 | player: VideoJsPlayer,
11 | callback: (currentPlayerTime: number) => void,
12 | ): () => void {
13 | const timeWatcherCallBack: () => void = () => callback(player.currentTime());
14 | player.on("timeupdate", timeWatcherCallBack);
15 | return timeWatcherCallBack;
16 | };
17 |
18 | /**
19 | * Deregisters a time watching obeserver from the current player
20 | * @param player The player to deregister the watcher.
21 | * @param callback regestered callBack function
22 | */
23 | export const deregisterTimeWatcher = function (player: VideoJsPlayer, callback: () => void) {
24 | player.off("timeupdate", callback);
25 | };
26 |
--------------------------------------------------------------------------------
/web/ts/worker.ts:
--------------------------------------------------------------------------------
1 | /** Make DELETE call to /api/workers/:id with given worker-id */
2 | export async function deleteWorker(id: string) {
3 | return await fetch("/api/workers/" + id, {
4 | method: "DELETE",
5 | });
6 | }
7 |
--------------------------------------------------------------------------------
/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "es6",
4 | "target": "es6",
5 | "sourceMap": true,
6 | "outDir": "assets/ts-dist",
7 | "noImplicitAny": false,
8 | "moduleResolution": "node",
9 | "sourceRoot": "./ts/",
10 | "rootDir": "./ts/",
11 | "lib": [
12 | "es2021",
13 | "dom"
14 | ],
15 | "allowSyntheticDefaultImports": true
16 | }
17 | }
--------------------------------------------------------------------------------
/web/web_logger.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import (
4 | "log/slog"
5 | "os"
6 | )
7 |
8 | var logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
9 | Level: slog.LevelDebug,
10 | })).With("service", "web")
11 |
--------------------------------------------------------------------------------
/web/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const { merge } = require("webpack-merge");
2 | const common = require("./webpack.common.js");
3 |
4 | module.exports = merge(common, {
5 | mode: "development",
6 | watch: true,
7 | devtool: "eval-source-map",
8 | devServer: {
9 | liveReload: true,
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/web/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const { merge } = require("webpack-merge");
2 | const common = require("./webpack.common.js");
3 |
4 | module.exports = merge(common, {
5 | mode: "production",
6 | optimization: {
7 | minimize: true,
8 | usedExports: true,
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/worker/.gitignore:
--------------------------------------------------------------------------------
1 | persist.gob
2 | mediamtx
3 |
--------------------------------------------------------------------------------
/worker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.24.1-alpine3.21 AS builder
2 |
3 |
4 | WORKDIR /go/src/github.com/TUM-Dev/gocast/worker
5 | COPY . .
6 |
7 | RUN GO111MODULE=on go mod download
8 | # bundle version into binary if specified in build-args, dev otherwise.
9 | ARG version=dev
10 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags "-w -extldflags '-static' -X main.VersionTag=${version}" -o /worker cmd/worker/worker.go
11 |
12 | FROM bluenviron/mediamtx:latest AS rtsp
13 |
14 | FROM alpine:3.18
15 | ADD entrypoint.sh /entrypoint.sh
16 | ADD mediamtx.yml /mediamtx.yml
17 | RUN chmod +x /entrypoint.sh
18 |
19 | RUN apk add --no-cache \
20 | ffmpeg \
21 | tzdata
22 |
23 | COPY --from=builder /worker /worker
24 | RUN chmod +x /worker
25 | COPY --from=rtsp /mediamtx /mediamtx
26 | RUN chmod +x /mediamtx
27 |
28 | CMD ["/entrypoint.sh"]
29 |
--------------------------------------------------------------------------------
/worker/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: all
2 | all: build
3 |
4 | VERSION := $(shell git rev-parse --short origin/HEAD)
5 |
6 | .PHONY: protoGen
7 | protoGen:
8 | cd api; \
9 | protoc ./api.proto --go-grpc_out=../.. --go_out=../..
10 |
11 | .PHONY: build
12 | build: deps
13 | go build -o main -ldflags="-X 'main.VersionTag=$(VERSION)'" cmd/worker/worker.go
14 |
15 | .PHONY: deps
16 | deps:
17 | go get ./...
18 |
19 | .PHONY: install
20 | install:
21 | mv main /bin/worker
22 |
23 | .PHONY: clean
24 | clean:
25 | rm -f main
26 |
27 | .PHONY: test
28 | test:
29 | go test -race ./...
30 |
31 | .PHONY: run
32 | run:
33 | go run cmd/worker/worker.go
34 |
35 |
36 | .PHONY: lint
37 | lint:
38 | golangci-lint run
39 |
--------------------------------------------------------------------------------
/worker/README.md:
--------------------------------------------------------------------------------
1 | # TUM-Live-Worker
2 |
3 | The worker for [TUM-Live](https://github.com/TUM-Dev/gocast)
4 |
5 | features include:
6 |
7 | - Record lecture halls
8 | - Transcode recordings
9 | - Push recordings to LRZ
10 | - Detect silence in recordings
11 | - Stream Video files as "Premieres"
12 |
--------------------------------------------------------------------------------
/worker/api/api_server_test.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "context"
5 | "github.com/TUM-Dev/gocast/worker/cfg"
6 | "github.com/TUM-Dev/gocast/worker/pb"
7 | "testing"
8 | )
9 |
10 | var mockServer = server{}
11 |
12 | func setup() {
13 | cfg.WorkerID = "123"
14 | }
15 |
16 | func TestServer_RequestStream(t *testing.T) {
17 | setup()
18 | _, err := mockServer.RequestStream(context.Background(), &pb.StreamRequest{
19 | WorkerId: "234"})
20 | if err == nil {
21 | t.Errorf("Request with wrong WorkerID should be rejected")
22 | return
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/worker/client/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "encoding/base64"
6 | "fmt"
7 | "github.com/TUM-Dev/gocast/worker/pb"
8 | log "github.com/sirupsen/logrus"
9 | "google.golang.org/grpc"
10 | "google.golang.org/grpc/backoff"
11 | "google.golang.org/grpc/credentials/insecure"
12 | "time"
13 | )
14 |
15 | // interactively test your implementation here
16 | func main() {
17 | c, err := dialIn("localhost")
18 | if err != nil {
19 | log.Fatal(err)
20 | }
21 | client := pb.NewToWorkerClient(c)
22 | waveform, err := client.RequestWaveform(context.Background(), &pb.WaveformRequest{
23 | File: "/srv/cephfs/livestream/rec/TUM-Live/2021/W/fpv/2021-10-22_08-30/fpv-2021-10-22-08-30COMB.mp4",
24 | WorkerId: "abc",
25 | })
26 | if err != nil {
27 | log.Fatal(err)
28 | }
29 | e := base64.StdEncoding.EncodeToString(waveform.Waveform)
30 | fmt.Println(e)
31 | }
32 |
33 | func dialIn(host string) (*grpc.ClientConn, error) {
34 | credentials := insecure.NewCredentials()
35 | conn, err := grpc.Dial(fmt.Sprintf("%s:50051", host), grpc.WithConnectParams(grpc.ConnectParams{
36 | Backoff: backoff.Config{
37 | BaseDelay: 1 * time.Second,
38 | Multiplier: 1.6,
39 | MaxDelay: 15 * time.Second,
40 | },
41 | }), grpc.WithTransportCredentials(credentials))
42 |
43 | return conn, err
44 | }
45 |
--------------------------------------------------------------------------------
/worker/edge/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.21-alpine3.18 as builder
2 |
3 | WORKDIR /go/src/github.com/TUM-Dev/gocast/worker
4 | COPY . .
5 |
6 | RUN GO111MODULE=on go mod download
7 |
8 | # bundle version into binary if specified in build-args, dev otherwise.
9 | ARG version=dev
10 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags "-w -extldflags '-static' -X main.VersionTag=${version}" -o /worker *.go &&\
11 | chmod +x /worker
12 |
13 | FROM alpine:3.18
14 | COPY --from=builder /worker /worker
15 |
16 | RUN apk add --no-cache ffmpeg
17 |
18 | CMD ["/worker"]
19 |
--------------------------------------------------------------------------------
/worker/edge/download.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 | "os/exec"
8 | "strings"
9 | )
10 |
11 | func downloadHandler(w http.ResponseWriter, r *http.Request) {
12 | var jwtClaims *JWTPlaylistClaims
13 | if jwtPubKey != nil {
14 | // validate token; every page access requires a valid jwt.
15 | if claims, ok := validateToken(w, r, true); !ok {
16 | return
17 | } else {
18 | jwtClaims = claims
19 | }
20 | }
21 | if jwtClaims != nil {
22 | vodsDownloaded.WithLabelValues(jwtClaims.StreamID, jwtClaims.CourseID).Inc()
23 | }
24 | w.Header().Add("Content-Type", "video/mp4")
25 | w.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", jwtClaims.GetFileName()))
26 | c := exec.Command("ffmpeg",
27 | "-i", "http://0.0.0.0"+port+strings.ReplaceAll(r.URL.String(), "download=1", ""),
28 | "-c", "copy", "-bsf:a", "aac_adtstoasc", "-movflags", "frag_keyframe", "-f", "mp4", "-")
29 | c.Stdout = w
30 | err := c.Start()
31 | if err != nil {
32 | log.Println(err.Error())
33 | return
34 | }
35 | err = c.Wait()
36 | if err != nil {
37 | log.Println(err.Error())
38 | return
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/worker/edge/edge:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/worker/edge/edge
--------------------------------------------------------------------------------
/worker/edge/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/TUM-Dev/gocast/worker/edge
2 |
3 | go 1.21
4 |
5 | require (
6 | github.com/golang-jwt/jwt/v5 v5.2.2
7 | github.com/prometheus/client_golang v1.21.1
8 | )
9 |
10 | require (
11 | github.com/beorn7/perks v1.0.1 // indirect
12 | github.com/cespare/xxhash/v2 v2.3.0 // indirect
13 | github.com/golang/protobuf v1.5.3 // indirect
14 | github.com/klauspost/compress v1.17.11 // indirect
15 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
16 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
17 | github.com/prometheus/client_model v0.6.1 // indirect
18 | github.com/prometheus/common v0.62.0 // indirect
19 | github.com/prometheus/procfs v0.15.1 // indirect
20 | golang.org/x/sys v0.28.0 // indirect
21 | google.golang.org/protobuf v1.36.1 // indirect
22 | )
23 |
--------------------------------------------------------------------------------
/worker/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | exec /mediamtx &
4 | exec /worker
5 |
--------------------------------------------------------------------------------
/worker/example.env:
--------------------------------------------------------------------------------
1 | Token=abc
2 | TempDir=./tmp
3 | StorageDir=./tmp
4 | LrzUser=Joscha Henningsen
5 | LrzMail=example@invalid
6 | LrzPhone=555-123-456
7 | LrzSubDir=RBG
8 | MainBase=localhost
9 | LrzUploadUrl=https://upload.example.lrz.de/cgi-bin
10 | SentryDSN=https://abc@123.ingest.sentry.io/123
11 | LogDir=./tmp
12 | LogLevel=debug
13 | VodURLTemplate=https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/%s.mp4/playlist.m3u8
14 |
--------------------------------------------------------------------------------
/worker/rtmp-relay/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.21 as authBuilder
2 |
3 | COPY main.go .
4 | RUN CGO_ENABLED=0 go build -ldflags="-extldflags=-static" -o /auth main.go
5 |
6 | FROM aler9/rtsp-simple-server:v1.0.0 AS rtsp
7 |
8 | FROM alpine:3.18
9 |
10 | COPY --from=rtsp /rtsp-simple-server /
11 | COPY --from=authBuilder /auth /auth
12 |
13 | WORKDIR /
14 | RUN apk add file
15 | ADD rtsp-simple-server.yml /rtsp-simple-server.yml
16 | ADD entrypoint.sh /entrypoint.sh
17 | RUN chmod +x /entrypoint.sh
18 |
19 | CMD [ "/entrypoint.sh" ]
20 |
--------------------------------------------------------------------------------
/worker/rtmp-relay/README.md:
--------------------------------------------------------------------------------
1 | # rtmp-relay
2 |
3 | This docker container acts as a rtmp receiver and offers a pullable stream via rtmp.
4 | This Server can be used as a lecture hall in TUM-Live as proxy to devices that only push rtmp streams.
5 |
6 | ## Usage
7 |
8 | edit `rtmp-simple-server.yml` and change the default username and password.
9 |
10 | ```bash
11 | docker build -t tumlive/rtmp-relay .
12 | docker run -p 1935:1935 -e VALID_PATHS=somestreamname,someotherstream tumlive/rtmp-relay
13 | ```
14 |
15 | Keep the VALID_PATHS secret, they allow streaming to the relay.
16 |
17 | Publishing a stream to `rtmp://localhost:1935/someValidPath` will make a pullable stream under `rtmp://localhost:1935/someValidPath` available.
18 |
--------------------------------------------------------------------------------
/worker/rtmp-relay/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | exec /rtsp-simple-server &
4 | exec /auth
5 |
--------------------------------------------------------------------------------
/worker/rtmp-relay/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net/http"
7 | "os"
8 | "strings"
9 | )
10 |
11 | // AuthReq is the request body sent by rtsp-simple-server to the auth app
12 | type AuthReq struct {
13 | Ip string `json:"ip"`
14 | User string `json:"user"`
15 | Password string `json:"password"`
16 | Path string `json:"path"`
17 | Protocol string `json:"protocol"`
18 | Id interface{} `json:"id"`
19 | Action string `json:"action"`
20 | Query string `json:"query"`
21 | }
22 |
23 | // this is an authentication app that replies with 200 if the publishing path is in the VALID_PATHS env var, >= 4ßß otherwise
24 | func main() {
25 | validPaths := strings.Split(os.Getenv("VALID_PATHS"), ",")
26 | fmt.Printf("Valid paths: %s\n", validPaths)
27 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
28 | var req AuthReq
29 | if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
30 | fmt.Println(err)
31 | w.WriteHeader(http.StatusBadRequest)
32 | return
33 | }
34 | for _, s := range validPaths {
35 | if s == req.Path {
36 | w.WriteHeader(http.StatusOK)
37 | return
38 | }
39 | }
40 | w.WriteHeader(http.StatusForbidden)
41 | })
42 |
43 | fmt.Println(http.ListenAndServe("127.0.0.1:9999", nil).Error())
44 | }
45 |
--------------------------------------------------------------------------------
/worker/tmp/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/worker/tmp/.gitkeep
--------------------------------------------------------------------------------
/worker/worker/ffmpegtools_test.go:
--------------------------------------------------------------------------------
1 | package worker
2 |
3 | import "testing"
4 |
5 | func TestGetDuration(t *testing.T) {
6 | duration, err := getDuration("testvid.mp4")
7 | if err != nil {
8 | t.Error(err)
9 | t.FailNow()
10 | }
11 | if duration != 1.000000 {
12 | t.Errorf("duration should be 1.000000 but is %f", duration)
13 | }
14 | }
15 |
16 | func TestGetVideoCodec(t *testing.T) {
17 | codec, err := getCodec("testvid.mp4", "video")
18 | if err != nil {
19 | t.Error(err)
20 | t.FailNow()
21 | }
22 | if codec != "h264" {
23 | t.Errorf("codec should be h264 but is %s", codec)
24 | }
25 | }
26 |
27 | func TestGetAudioCodec(t *testing.T) {
28 | codec, err := getCodec("testvid.mp4", "audio")
29 | if err != nil {
30 | t.Error(err)
31 | t.FailNow()
32 | }
33 | if codec != "aac" {
34 | t.Errorf("codec should be aac but is %s", codec)
35 | }
36 | }
37 |
38 | func TestGetLevel(t *testing.T) {
39 | l, err := getLevel("testvid.mp4")
40 | if err != nil {
41 | t.Error(err)
42 | t.FailNow()
43 | }
44 | if l != "30" {
45 | t.Errorf("level should be 30 but is %s", l)
46 | }
47 | }
48 |
49 | func TestGetContainer(t *testing.T) {
50 | c, err := getContainer("testvid.mp4")
51 | if err != nil {
52 | t.Error(err)
53 | t.FailNow()
54 | }
55 | if c != "mov,mp4,m4a,3gp,3g2,mj2" {
56 | t.Errorf("codec should be mov,mp4,m4a,3gp,3g2,mj2 but is %s", c)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/worker/worker/premiere.go:
--------------------------------------------------------------------------------
1 | package worker
2 |
3 | import (
4 | "fmt"
5 | "github.com/TUM-Dev/gocast/worker/cfg"
6 | log "github.com/sirupsen/logrus"
7 | "os"
8 | "os/exec"
9 | )
10 |
11 | func streamPremiere(ctx *StreamContext) {
12 | // we're a little paranoid about our input as we can't control it:
13 | cmd := exec.Command(
14 | "ffmpeg", "-re", "-i", ctx.sourceUrl,
15 | "-pix_fmt", "yuv420p", "-vsync", "1", "-threads", "0", "-vcodec", "libx264",
16 | "-r", "30", "-g", "60", "-sc_threshold", "0",
17 | "-b:v", "2500k", "-bufsize", "3000k", "-maxrate", "3000k",
18 | "-preset", "veryfast", "-profile:v", "baseline", "-tune", "film",
19 | "-acodec", "aac", "-b:a", "128k", "-ac", "2", "-ar", "48000", "-af", "aresample=async=1:min_hard_comp=0.100000:first_pts=0",
20 | "-f", "flv", fmt.Sprintf("%s%s", ctx.ingestServer, ctx.streamName))
21 | log.WithField("cmd", cmd.String()).Info("Starting premiere")
22 | ffmpegErr, errFfmpegErrFile := os.OpenFile(fmt.Sprintf("%s/ffmpeg_%s.log", cfg.LogDir, ctx.getStreamName()), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
23 | if errFfmpegErrFile == nil {
24 | cmd.Stderr = ffmpegErr
25 | } else {
26 | log.WithError(errFfmpegErrFile).Error("Could not create file for ffmpeg stdErr")
27 | }
28 | err := cmd.Run()
29 | if err != nil {
30 | log.WithError(err).Error("Can't stream premiere")
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/worker/worker/testvid.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TUM-Dev/gocast/a146578a7c99b77a29c33daf4d7cdd5bb0fa67f0/worker/worker/testvid.mp4
--------------------------------------------------------------------------------
/worker/worker/vmstat/vmstat_test.go:
--------------------------------------------------------------------------------
1 | package vmstat
2 |
3 | import "testing"
4 |
5 | func TestVmStat_GetCpuStr(t *testing.T) {
6 | s := New()
7 | s.CpuPercent = 42
8 | if s.GetCpuStr() != "42%" {
9 | t.Error("GetCpuStr() failed")
10 | }
11 | }
12 |
13 | func TestVmStat_GetMemStr(t *testing.T) {
14 | s := New()
15 | s.MemAvailable = 28690272256
16 | s.MemTotal = 33638785024
17 | if s.GetMemStr() != "28.690M/33.638M (15%)" {
18 | t.Error("GetMemStr() failed, got:", s.GetMemStr(), "expected: 28.690M/33.638M (15%)")
19 | }
20 | }
21 |
22 | func TestVmStat_GetDiskStr(t *testing.T) {
23 | s := New()
24 | s.DiskTotal = 974437085184
25 | s.DiskUsed = 287634259968
26 | s.DiskPercent = 31.100071498992595
27 | if s.GetDiskStr() != "287G/974G (31%)" {
28 | t.Error("GetDiskStr() failed, got:", s.GetDiskStr(), "expected: 28.690M/33.638M (15%)")
29 | }
30 | }
31 |
--------------------------------------------------------------------------------