From a0d815f92cde11e573a8dd1b9cc44e689284e165 Mon Sep 17 00:00:00 2001 From: Matthew Humphreys Date: Fri, 19 Jan 2024 12:55:10 +0100 Subject: [PATCH] Skip initial silence (#1262) * added api endpoints for user setting * fixed API endpoints * watchPageData sets progress to end of first silence when user setting is set * GetAutoSkipEnabled wraps result in json * added front-end component * added error handling when getting user setting * make sure stream is not selfstream before skipping silence * imported logger * gofumpt everything * updated db starter for better testing * use correct logger Co-authored-by: Joscha Henningsen <44805696+joschahenningsen@users.noreply.github.com> * use correct logger Co-authored-by: Joscha Henningsen <44805696+joschahenningsen@users.noreply.github.com> --------- Co-authored-by: Joscha Henningsen <44805696+joschahenningsen@users.noreply.github.com> --- api/live_update.go | 6 ++++-- api/users.go | 32 +++++++++++++++++++++++++++++++ api/worker_grpc.go | 2 +- cmd/modelGen/modelGen.go | 4 ++-- docs/static/tum-live-starter.sql | 4 +++- model/stream.go | 15 +++++++++++++++ model/user.go | 21 ++++++++++++++++++++ tools/config.go | 6 ++++-- web/template/user-settings.gohtml | 15 +++++++++++++++ web/ts/user-settings.ts | 1 + web/watch.go | 12 ++++++++++++ worker/cfg/cfg.go | 4 ++-- worker/edge/edge.go | 2 +- worker/worker/persist.go | 2 +- worker/worker/premiere.go | 2 +- worker/worker/stream.go | 2 +- worker/worker/thumbnails.go | 2 +- worker/worker/transcode.go | 4 ++-- worker/worker/upload_test.go | 2 +- 19 files changed, 120 insertions(+), 18 deletions(-) diff --git a/api/live_update.go b/api/live_update.go index 7b7c756cf..ba93f0c4f 100644 --- a/api/live_update.go +++ b/api/live_update.go @@ -20,8 +20,10 @@ const ( UpdateTypeCourseWentLive = "course_went_live" ) -var liveUpdateListenerMutex sync.RWMutex -var liveUpdateListener = map[uint]*liveUpdateUserSessionsWrapper{} +var ( + liveUpdateListenerMutex sync.RWMutex + liveUpdateListener = map[uint]*liveUpdateUserSessionsWrapper{} +) type liveUpdateUserSessionsWrapper struct { sessions []*realtime.Context diff --git a/api/users.go b/api/users.go index 2afc0e454..175a70eff 100644 --- a/api/users.go +++ b/api/users.go @@ -27,6 +27,7 @@ func configGinUsersRouter(router *gin.Engine, daoWrapper dao.DaoWrapper) { router.POST("/api/users/settings/playbackSpeeds", routes.updatePlaybackSpeeds) router.POST("/api/users/settings/seekingTime", routes.updateSeekingTime) router.POST("/api/users/settings/customSpeeds", routes.updateCustomSpeeds) + router.POST("/api/users/settings/autoSkip", routes.updateAutoSkip) router.POST("/api/users/resetPassword", routes.resetPassword) @@ -723,6 +724,37 @@ func (r usersRoutes) updateSeekingTime(c *gin.Context) { } } +func (r usersRoutes) updateAutoSkip(c *gin.Context) { + u := c.MustGet("TUMLiveContext").(tools.TUMLiveContext).User + if u == nil { + _ = c.Error(tools.RequestError{ + Status: http.StatusUnauthorized, + CustomMessage: "login required", + }) + return + } + var req struct{ Value model.AutoSkipSetting } + if err := c.BindJSON(&req); err != nil { + _ = c.Error(tools.RequestError{ + Status: http.StatusBadRequest, + CustomMessage: "can not bind body to request", + Err: err, + }) + return + } + + settingBytes, _ := json.Marshal(req.Value) + err := r.DaoWrapper.UsersDao.AddUserSetting(&model.UserSetting{UserID: u.ID, Type: model.AutoSkip, Value: string(settingBytes)}) + if err != nil { + _ = c.Error(tools.RequestError{ + Status: http.StatusInternalServerError, + CustomMessage: "can not add user setting", + Err: err, + }) + return + } +} + func (r usersRoutes) exportPersonalData(c *gin.Context) { var resp personalData u := c.MustGet("TUMLiveContext").(tools.TUMLiveContext).User diff --git a/api/worker_grpc.go b/api/worker_grpc.go index 13c909055..4db638931 100644 --- a/api/worker_grpc.go +++ b/api/worker_grpc.go @@ -882,7 +882,7 @@ func getLivePreviewFromWorker(s *model.Stream, workerID string, client pb.ToWork return err } - if err := os.MkdirAll(pathprovider.TUMLiveTemporary, 0750); err != nil { + if err := os.MkdirAll(pathprovider.TUMLiveTemporary, 0o750); err != nil { return err } diff --git a/cmd/modelGen/modelGen.go b/cmd/modelGen/modelGen.go index a3dd1f9b2..c537a1594 100644 --- a/cmd/modelGen/modelGen.go +++ b/cmd/modelGen/modelGen.go @@ -28,7 +28,7 @@ func main() { } fmt.Println("Generating model...") - model_file, err := os.OpenFile(fmt.Sprintf("model/%s.go", d.NamePrivate), os.O_WRONLY|os.O_CREATE, 0644) + model_file, err := os.OpenFile(fmt.Sprintf("model/%s.go", d.NamePrivate), os.O_WRONLY|os.O_CREATE, 0o644) if err != nil { fmt.Println(err) os.Exit(1) @@ -46,7 +46,7 @@ func main() { fmt.Println("Generating dao...") - dao_file, err := os.OpenFile(fmt.Sprintf("dao/%s.go", d.NamePrivate), os.O_WRONLY|os.O_CREATE, 0644) + dao_file, err := os.OpenFile(fmt.Sprintf("dao/%s.go", d.NamePrivate), os.O_WRONLY|os.O_CREATE, 0o644) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/docs/static/tum-live-starter.sql b/docs/static/tum-live-starter.sql index fcd1ae2ad..41044ecab 100644 --- a/docs/static/tum-live-starter.sql +++ b/docs/static/tum-live-starter.sql @@ -352,6 +352,7 @@ CREATE TABLE `lecture_halls` ( LOCK TABLES `lecture_halls` WRITE; /*!40000 ALTER TABLE `lecture_halls` DISABLE KEYS */; +INSERT INTO `lecture_halls` VALUES (1,NULL,NULL,NULL,'HS001','Hörsaal 001',NULL,NULL,NULL,NULL,NULL,NULL,NULL); /*!40000 ALTER TABLE `lecture_halls` ENABLE KEYS */; UNLOCK TABLES; @@ -606,6 +607,7 @@ CREATE TABLE `silences` ( LOCK TABLES `silences` WRITE; /*!40000 ALTER TABLE `silences` DISABLE KEYS */; +INSERT INTO `silences` VALUES (1,'2024-01-14 21:16:37.000','2024-01-14 21:16:43.000',NULL,0,100,1),(2,'2024-01-14 21:17:00.000','2024-01-14 21:17:02.000',NULL,0,200,2); /*!40000 ALTER TABLE `silences` ENABLE KEYS */; UNLOCK TABLES; @@ -813,7 +815,7 @@ CREATE TABLE `streams` ( LOCK TABLES `streams` WRITE; /*!40000 ALTER TABLE `streams` DISABLE KEYS */; -INSERT INTO `streams` VALUES (1,'2022-04-18 13:45:58.657','2022-04-18 13:46:46.547',NULL,'VL 1: Was ist Bier?','',1,'2022-04-11 12:00:00.000','2022-04-11 13:00:00.000','','','',0,NULL,'c33dfc976efb410299e604b255db0127','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',0,1,NULL,NULL,0,NULL,NULL,NULL,0,'',NULL),(2,'2022-04-18 13:46:25.841','2022-04-18 13:46:46.547',NULL,'VL 2: Wie mache ich Bier?','',1,'2022-04-18 12:00:00.000','2022-04-18 13:00:00.000','','','',0,NULL,'5815366e4010482687912588349bc5c0','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',0,1,NULL,NULL,0,NULL,NULL,NULL,0,'',NULL),(4,'2022-04-18 13:46:46.547','2022-04-18 13:46:46.547',NULL,'VL 3: Rückblick','',1,'2026-02-19 12:00:00.000','2026-02-19 13:00:00.000','','','',0,NULL,'d8ce0b882dbc4d999b42c143ce07db5a','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',0,0,NULL,NULL,0,NULL,NULL,NULL,0,'',NULL),(7,'2022-04-18 13:46:46.547','2022-04-18 13:46:46.547',NULL,'VL 1: Livestream','',2,'2022-02-19 12:00:00.000','2022-02-19 13:00:00.000','','','',0,NULL,'','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',1,0,NULL,NULL,0,NULL,NULL,NULL,0,'',NULL),(8,'2022-04-18 13:46:46.547','2022-04-18 13:46:46.547',NULL,'VL 1: Intro to Go','',3,'2022-02-19 12:00:00.000','2022-02-19 12:00:00.000','','','',0,NULL,'d8ce0b882dbc4d999b42c143ce07db5a','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',0,1,NULL,NULL,0,NULL,NULL,NULL,0,'',NULL); +INSERT INTO `streams` VALUES (1,'2022-04-18 13:45:58.657','2022-04-18 13:46:46.547',NULL,'VL 1: Was ist Bier?','',1,'2022-04-11 12:00:00.000','2022-04-11 12:09:56.000','','','',0,NULL,'c33dfc976efb410299e604b255db0127','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',0,1,NULL,NULL,0,NULL,NULL,1,0,'',NULL),(2,'2022-04-18 13:46:25.841','2022-04-18 13:46:46.547',NULL,'VL 2: Wie mache ich Bier?','',1,'2022-04-18 12:00:00.000','2022-04-18 12:09:56.000','','','',0,NULL,'5815366e4010482687912588349bc5c0','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',0,1,NULL,NULL,0,NULL,NULL,1,0,'',NULL),(4,'2022-04-18 13:46:46.547','2022-04-18 13:46:46.547',NULL,'VL 3: Rückblick','',1,'2026-02-19 12:00:00.000','2026-02-19 13:00:00.000','','','',0,NULL,'d8ce0b882dbc4d999b42c143ce07db5a','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',0,0,NULL,NULL,0,NULL,NULL,1,0,'',NULL),(7,'2022-04-18 13:46:46.547','2022-04-18 13:46:46.547',NULL,'VL 1: Livestream','',2,'2022-02-19 12:00:00.000','2022-02-19 13:00:00.000','','','',0,NULL,'','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',1,0,NULL,NULL,0,NULL,NULL,1,0,'',NULL),(8,'2022-04-18 13:46:46.547','2022-04-18 13:46:46.547',NULL,'VL 1: Intro to Go','',3,'2022-02-19 12:00:00.000','2022-02-19 12:00:00.000','','','',0,NULL,'d8ce0b882dbc4d999b42c143ce07db5a','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','https://stream.lrz.de/vod/_definst_/mp4:tum/RBG/bb.mp4/playlist.m3u8','',0,1,NULL,NULL,0,NULL,NULL,1,0,'',NULL); /*!40000 ALTER TABLE `streams` ENABLE KEYS */; UNLOCK TABLES; diff --git a/model/stream.go b/model/stream.go index ba717b9ca..fc3572c71 100755 --- a/model/stream.go +++ b/model/stream.go @@ -410,3 +410,18 @@ func (s Stream) ToDTO() StreamDTO { Duration: duration, } } + +// FirstSilenceAsProgress returns the end of the first silence as a quotient of the length of the stream +func (s Stream) FirstSilenceAsProgress() float64 { + if len(s.Silences) == 0 { + return 0 + } + // Sanity check: first silence at beginning of stream + if s.Silences[0].Start != 0 { + return 0 + } + duration := s.End.Sub(s.Start).Seconds() + p := float64(s.Silences[0].End) / duration + + return p +} diff --git a/model/user.go b/model/user.go index 999adc4ab..5b30425b8 100755 --- a/model/user.go +++ b/model/user.go @@ -58,6 +58,7 @@ const ( CustomPlaybackSpeeds SeekingTime UserDefinedSpeeds + AutoSkip ) type UserSetting struct { @@ -197,6 +198,26 @@ func (u User) PreferredNameChangeAllowed() bool { return true } +// AutoSkipSetting wraps whether auto skip is enabled in JSON +type AutoSkipSetting struct { + Enabled bool `json:"enabled"` +} + +// GetAutoSkipEnabled returns whether the user has enabled auto skip +func (u User) GetAutoSkipEnabled() (AutoSkipSetting, error) { + for _, setting := range u.Settings { + if setting.Type == AutoSkip { + var a AutoSkipSetting + err := json.Unmarshal([]byte(setting.Value), &a) + if err != nil { + return AutoSkipSetting{Enabled: false}, err + } + return a, nil + } + } + return AutoSkipSetting{Enabled: false}, nil +} + type argonParams struct { memory uint32 iterations uint32 diff --git a/tools/config.go b/tools/config.go index 8d2190007..f4c49aad3 100644 --- a/tools/config.go +++ b/tools/config.go @@ -15,8 +15,10 @@ import ( "github.com/spf13/viper" ) -var Cfg Config -var Loc *time.Location +var ( + Cfg Config + Loc *time.Location +) func LoadConfig() { initCache() diff --git a/web/template/user-settings.gohtml b/web/template/user-settings.gohtml index b24214b21..b3857a061 100644 --- a/web/template/user-settings.gohtml +++ b/web/template/user-settings.gohtml @@ -139,6 +139,21 @@ +
+

Automatically Skip First Silence

+ + + +

Privacy & Data Protection

{ diff --git a/web/watch.go b/web/watch.go index b966e7c12..f866d0f10 100644 --- a/web/watch.go +++ b/web/watch.go @@ -3,6 +3,7 @@ package web import ( "errors" "html/template" + "math" "net/http" "strconv" "strings" @@ -93,6 +94,17 @@ func (r mainRoutes) WatchPage(c *gin.Context) { } else if len(progress) > 0 { data.Progress = progress[0] } + + // Check if user wants to skip first silence + autoSkip, err := tumLiveContext.User.GetAutoSkipEnabled() + if err != nil { + logger.Error("Couldn't decode user setting", "err", err) + } else if autoSkip.Enabled { + // The length of the stream may mismatch with the length of the video if it is a self-stream + if tumLiveContext.Stream.LectureHallID != 0 { + data.Progress.Progress = math.Max(data.Progress.Progress, tumLiveContext.Stream.FirstSilenceAsProgress()) + } + } } if c.Query("restart") == "1" { c.Redirect(http.StatusFound, strings.Split(c.Request.RequestURI, "?")[0]) diff --git a/worker/cfg/cfg.go b/worker/cfg/cfg.go index baf6f1b24..051f3d93d 100644 --- a/worker/cfg/cfg.go +++ b/worker/cfg/cfg.go @@ -77,11 +77,11 @@ func SetConfig() { if PersistDir == "" { PersistDir = "." } - err := os.MkdirAll(PersistDir, 0755) + err := os.MkdirAll(PersistDir, 0o755) if err != nil { log.Error(err) } - err = os.MkdirAll(LogDir, 0755) + err = os.MkdirAll(LogDir, 0o755) if err != nil { log.Warn("Could not create log directory: ", err) } diff --git a/worker/edge/edge.go b/worker/edge/edge.go index 867ec1160..a29682d2d 100644 --- a/worker/edge/edge.go +++ b/worker/edge/edge.go @@ -374,7 +374,7 @@ func fetchFile(host, file string) error { return fmt.Errorf("parse file path: %s", file) } d := filepath.Dir(diskDir) - err = os.MkdirAll(d, 0755) + err = os.MkdirAll(d, 0o755) if err != nil { return err } diff --git a/worker/worker/persist.go b/worker/worker/persist.go index 8da624d71..8eb45f733 100644 --- a/worker/worker/persist.go +++ b/worker/worker/persist.go @@ -26,7 +26,7 @@ const persistFileName = "/persist.gob" // writeOut writes out the persistable object to disk func (p *Persistable) writeOut() error { - f, err := os.OpenFile(cfg.PersistDir+persistFileName, os.O_RDWR|os.O_CREATE, 0666) + f, err := os.OpenFile(cfg.PersistDir+persistFileName, os.O_RDWR|os.O_CREATE, 0o666) if err != nil { return err } diff --git a/worker/worker/premiere.go b/worker/worker/premiere.go index 856230c31..56d3a7130 100644 --- a/worker/worker/premiere.go +++ b/worker/worker/premiere.go @@ -20,7 +20,7 @@ func streamPremiere(ctx *StreamContext) { "-acodec", "aac", "-b:a", "128k", "-ac", "2", "-ar", "48000", "-af", "aresample=async=1:min_hard_comp=0.100000:first_pts=0", "-f", "flv", fmt.Sprintf("%s%s", ctx.ingestServer, ctx.streamName)) log.WithField("cmd", cmd.String()).Info("Starting premiere") - ffmpegErr, errFfmpegErrFile := os.OpenFile(fmt.Sprintf("%s/ffmpeg_%s.log", cfg.LogDir, ctx.getStreamName()), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) + ffmpegErr, errFfmpegErrFile := os.OpenFile(fmt.Sprintf("%s/ffmpeg_%s.log", cfg.LogDir, ctx.getStreamName()), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644) if errFfmpegErrFile == nil { cmd.Stderr = ffmpegErr } else { diff --git a/worker/worker/stream.go b/worker/worker/stream.go index 8c73c2294..ebe062909 100644 --- a/worker/worker/stream.go +++ b/worker/worker/stream.go @@ -45,7 +45,7 @@ func stream(streamCtx *StreamContext) { // persist stream command in context, so it can be killed later streamCtx.streamCmd = cmd log.WithField("cmd", cmd.String()).Info("Starting stream") - ffmpegErr, errFfmpegErrFile := os.OpenFile(fmt.Sprintf("%s/ffmpeg_%s.log", cfg.LogDir, streamCtx.getStreamName()), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) + ffmpegErr, errFfmpegErrFile := os.OpenFile(fmt.Sprintf("%s/ffmpeg_%s.log", cfg.LogDir, streamCtx.getStreamName()), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644) if errFfmpegErrFile == nil { cmd.Stderr = ffmpegErr } else { diff --git a/worker/worker/thumbnails.go b/worker/worker/thumbnails.go index 9f212d97b..f621028c7 100644 --- a/worker/worker/thumbnails.go +++ b/worker/worker/thumbnails.go @@ -50,7 +50,7 @@ func createVideoThumbnail(ctx *StreamContext, source string) error { if err != nil { return err } - file, err := os.OpenFile(ctx.getLargeThumbnailSpriteFileName(), os.O_CREATE|os.O_WRONLY, 0644) + file, err := os.OpenFile(ctx.getLargeThumbnailSpriteFileName(), os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return err } diff --git a/worker/worker/transcode.go b/worker/worker/transcode.go index ed221dcb0..a58c0a94b 100644 --- a/worker/worker/transcode.go +++ b/worker/worker/transcode.go @@ -149,7 +149,7 @@ func handleTranscodingOutput(stderr io.ReadCloser, inputTime float64, progressCh // creates folder for output file if it doesn't exist func prepare(out string) error { dir := filepath.Dir(out) - err := os.MkdirAll(dir, 0750) + err := os.MkdirAll(dir, 0o750) if err != nil { return fmt.Errorf("create output directory for transcoding: %s", err) } @@ -159,7 +159,7 @@ func prepare(out string) error { // markForDeletion moves the file to $recfolder/.trash/ func markForDeletion(ctx *StreamContext) error { trashName := ctx.getRecordingTrashName() - err := os.MkdirAll(filepath.Dir(trashName), 0750) + err := os.MkdirAll(filepath.Dir(trashName), 0o750) if err != nil { return fmt.Errorf("create trash directory: %s", err) } diff --git a/worker/worker/upload_test.go b/worker/worker/upload_test.go index 763eb5970..416cebb88 100644 --- a/worker/worker/upload_test.go +++ b/worker/worker/upload_test.go @@ -72,7 +72,7 @@ func createDummyFile(filesize uint) (string, error) { if err != nil { return "", err } - f, err := os.OpenFile(file.Name(), os.O_APPEND|os.O_WRONLY, 0600) + f, err := os.OpenFile(file.Name(), os.O_APPEND|os.O_WRONLY, 0o600) if err != nil { return "", err }