├── .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 | 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 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/prettier.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/protoeditor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | 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 | 4 | 5 | 6 | 7 | 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 | ![sections on watch page](video-img/sections-on-watch-page.jpg) 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 | ![video-sections](video-img/video-sections.jpg) -------------------------------------------------------------------------------- /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 | ![test](/img/showcase-02.png#showcase) 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 | ![test](/img/showcase-03.png#showcase) 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 | ![sections on watch page](video-img/sections-on-watch-page.jpg) 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 | ![video-sections](video-img/video-sections.jpg) -------------------------------------------------------------------------------- /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 | ![runner.png](/deployment/DeploymentDiagram.png) 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 | ![test](/img/showcase-02.png#showcase) 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 | ![test](/img/showcase-03.png#showcase) 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 | ![sections on watch page](video-img/sections-on-watch-page.jpg) 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 | ![video-sections](video-img/video-sections.jpg) -------------------------------------------------------------------------------- /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 |
17 |
18 | 21 |
22 |
23 |
24 |
25 | {{.Text}} 26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /web/template/partial/close-btn.gohtml: -------------------------------------------------------------------------------- 1 | {{define "close-button"}} 2 |
3 | 9 |
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 | --------------------------------------------------------------------------------