commit 751b779d8e7ce976d47f463ae36f64e9fe98c067 Author: Jan-Lukas Else Date: Wed May 20 17:50:37 2020 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07706b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.idea +bbb-download +*.mp4 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5442b9f --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# bbb-download + +This is a downloader for BigBlueButton. It's based on [this code](https://github.com/zakcali/bbb-download) but with a few tweaks and support for downloading the screenshare (deskshare) recordings. \ No newline at end of file diff --git a/bbb-download.go b/bbb-download.go new file mode 100644 index 0000000..604a4cc --- /dev/null +++ b/bbb-download.go @@ -0,0 +1,321 @@ +package main + +import ( + "bufio" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "math" + "net/http" + "os" + "os/exec" + "path" + "strconv" + "strings" +) + +func main() { + fmt.Println("BigBlueButton video creator/downloader") + + scanner := bufio.NewScanner(os.Stdin) + + fmt.Print("Enter URL of recording: ") + scanner.Scan() + presentationUrl := scanner.Text() + result := strings.SplitAfter(presentationUrl, "?meetingId=") + presentationId := result[1] + + fmt.Print("Name of recording: ") + scanner.Scan() + meetingName := scanner.Text() + + result2 := strings.Split(result[0], "/playback/") + baseUrl := result2[0] + "/presentation/" + presentationId + fmt.Println("baseUrl= ", baseUrl) + + //read duration of recording and meeting name from meta.xml + metaUrl := baseUrl + "/metadata.xml" + responseMeta, err := http.Get(metaUrl) + if err != nil { + log.Fatal(err) + } + defer func() { + _ = responseMeta.Body.Close() + }() + + metaBody, err := ioutil.ReadAll(responseMeta.Body) + if err != nil { + log.Fatal(err) + } + //finding correct duration-ending of last slide + timeString := strings.SplitAfter(string(metaBody), "") + duration := strings.Split(timeString[1], "") + fmt.Println("duration of recording=", duration[0], "ms") + + shapesUrl := baseUrl + "/shapes.svg" + webcamsWebmUrl := baseUrl + "/video/webcams.webm" + webcamsMp4Url := baseUrl + "/video/webcams.mp4" + deskshareWebmUrl := baseUrl + "/deskshare/deskshare.webm" + deskshareMp4Url := baseUrl + "/deskshare/deskshare.mp4" + + //read content of the shapes.svg file, and assign shapes to it + responseShapes, err := http.Get(shapesUrl) + if err != nil { + log.Fatal(err) + } + defer func() { + _ = responseShapes.Body.Close() + }() + + shapesBody, err := ioutil.ReadAll(responseShapes.Body) + if err != nil { + log.Fatal(err) + } + shapes := string(shapesBody) + + fmt.Println("creating directory: ", presentationId) + if _, err := os.Stat(presentationId); os.IsNotExist(err) { + err = os.Mkdir(presentationId, 0700) + if err != nil { + fmt.Println("Failed to create temporary folder:", err.Error()) + } + } + + // Find and print slide timings, image Urls + durations := make(map[int]float64) + vidnames := make(map[int]string) + imgnames := make(map[int]string) + inValue, outValue, truncated := 0.0, 0.0, 0.0 + inSrc, outSrc, pngSrc := "0.0", "10.5", "presentation/" + i := 1 // number of png pictures for slide + + //parse for in= out= href= from /shapes.svg + ins := strings.Split(shapes, "in=\"") + outs := strings.Split(shapes, "out=\"") + pngs := strings.Split(shapes, "xlink:href=\"") + + for k := 1; k < len(ins); k++ { + intext := strings.SplitAfter(ins[k], "\"") + realin := strings.Split(intext[0], "\"") + inSrc = realin[0] + + outtext := strings.SplitAfter(outs[k], "\"") + realout := strings.Split(outtext[0], "\"") + outSrc = realout[0] + + imgtext := strings.SplitAfter(pngs[k], "\"") + realpng := strings.Split(imgtext[0], "\"") + pngSrc = realpng[0] + + inValue, _ = strconv.ParseFloat(inSrc, 64) + outValue, _ = strconv.ParseFloat(outSrc, 64) + truncated = (outValue*10 - inValue*10) / 10 + durations[i] = truncated + imgnames[i] = "s" + strconv.Itoa(i) + ".png" + vidnames[i] = "v" + strconv.Itoa(i) + ".mp4" + + imgUrl := baseUrl + "/" + pngSrc + + if err := DownloadFile(path.Join(presentationId+"/"+imgnames[i]), imgUrl); err != nil { + panic(err) + } + i++ + } + + //correct duration of last slide + outValue, _ = strconv.ParseFloat(duration[0], 64) + outValue = outValue / 1000 + outValue = math.Round(outValue*100) / 100 + fmt.Println("ending of presentation=", outValue) + truncated = (outValue*10 - inValue*10) / 10 + durations[i-1] = math.Round(truncated*100) / 100 + fmt.Println("Duration of last slide according to meta.xml is: ", durations[i-1]) + + // create mp4 files from png files + fmt.Println("Creating videos from slide pictures, duration is given as seconds") + for j := 1; j < i; j++ { + fmt.Println(imgnames[j], " ", vidnames[j], " ", durations[j]) + cmd := exec.Command("ffmpeg", + "-loop", "1", + "-framerate", "1", + "-i", path.Join(presentationId, imgnames[j]), + "-t", fmt.Sprint(durations[j]), + "-pix_fmt", "yuv420p", + "-vf", "scale=-1:720", + path.Join(presentationId, vidnames[j])) + err = cmd.Run() + if err != nil { + fmt.Println("Failed to create video from slide:", err.Error()) + return + } + } + + //create video_list.txt file to cancat with ffmpeg + f, err := os.Create("video_list.txt") + if err != nil { + fmt.Println(err) + return + } + for j := 1; j < i; j++ { + _, err := f.WriteString("file " + path.Join(presentationId, vidnames[j]) + "\n") + if err != nil { + fmt.Println(err) + _ = f.Close() + return + } + } + err = f.Close() + if err != nil { + fmt.Println(err) + return + } + + // concat slide videos to create one piece of video file: slides.mp4 + fmt.Println("merging slide videos to create: slides.mp4") + cmd := exec.Command("ffmpeg", + "-f", "concat", + "-safe", "0", + "-i", "video_list.txt", + "-c", "copy", + path.Join(presentationId, "slides.mp4")) + err = cmd.Run() + if err != nil { + fmt.Println("Failed to merge slide videos:", err.Error()) + return + } + fmt.Println("slide videos merged") + + // download webcams + fmt.Println("downloading webcams") + err = DownloadFile(path.Join(presentationId, "webcams.webm"), webcamsWebmUrl) + if err != nil { + err = DownloadFile(path.Join(presentationId, "orig-webcams.mp4"), webcamsMp4Url) + if err != nil { + fmt.Println("Failed to download webcams:", err.Error()) + return + } + cmd = exec.Command("ffmpeg", + "-i", path.Join(presentationId, "orig-webcams.mp4"), + "-framerate", "1", + "-q:a", "0", + "-q:v", "0", + "-vf", "scale=-1:720", + path.Join(presentationId, "webcams.mp4")) + err = cmd.Run() + if err != nil { + fmt.Println("Failed to convert orig-webcams.mp4 to MP4:", err.Error()) + return + } + } else { + // Successful download, convert to MP4 + fmt.Println("webcams.webm file is downloaded") + fmt.Println("converting webcams.webm to webcams.mp4") + cmd = exec.Command("ffmpeg", + "-i", path.Join(presentationId, "webcams.webm"), + "-framerate", "1", + "-q:a", "0", + "-q:v", "0", + "-vf", "scale=-1:720", + path.Join(presentationId, "webcams.mp4")) + err = cmd.Run() + if err != nil { + fmt.Println("Failed to convert webcams.webm to MP4:", err.Error()) + return + } + } + + // download deskshare + deskshare := true + fmt.Println("downloading deskshare") + err = DownloadFile(path.Join(presentationId, "deskshare.webm"), deskshareWebmUrl) + if err != nil { + err = DownloadFile(path.Join(presentationId, "orig-deskshare.mp4"), deskshareMp4Url) + if err != nil { + fmt.Println("No deskshare") + deskshare = false + } else { + cmd = exec.Command("ffmpeg", + "-i", path.Join(presentationId, "orig-deskshare.mp4"), + "-framerate", "1", + "-q:a", "0", + "-q:v", "0", + "-vf", "scale=-1:720", + path.Join(presentationId, "deskshare.mp4")) + err = cmd.Run() + if err != nil { + fmt.Println("Failed to convert orig-deskshare.mp4 to MP4:", err.Error()) + return + } + } + } else { + // Successful download, convert to MP4 + fmt.Println("deskshare.webm file is downloaded") + fmt.Println("converting deskshare.webm to deskshare.mp4") + cmd = exec.Command("ffmpeg", + "-i", path.Join(presentationId, "deskshare.webm"), + "-framerate", "1", + "-q:a", "0", + "-q:v", "0", + "-vf", "scale=-1:720", + path.Join(presentationId, "deskshare.mp4")) + err = cmd.Run() + if err != nil { + fmt.Println("Failed to convert deskshare.webm to MP4:", err.Error()) + return + } + } + + fmt.Println("merging slides, webcams and deskshare side by side") + cmd = exec.Command("ffmpeg", + "-i", path.Join(presentationId, "slides.mp4"), + "-i", path.Join(presentationId, "webcams.mp4"), + "-i", path.Join(presentationId, "deskshare.mp4"), + "-framerate", "1", + "-filter_complex", "hstack=inputs=3", + meetingName+".mp4") + if !deskshare { + cmd = exec.Command("ffmpeg", + "-i", path.Join(presentationId, "slides.mp4"), + "-i", path.Join(presentationId, "webcams.mp4"), + "-framerate", "1", + "-filter_complex", "hstack=inputs=2", + meetingName+".mp4") + } + err = cmd.Run() + if err != nil { + fmt.Println("Failed to merge slides and webcams:", err.Error()) + return + } + + fmt.Println("Name of the final video is: ", meetingName) + + _ = os.RemoveAll(presentationId + "/") + _ = os.Remove("video_list.txt") +} + +// DownloadFile will download a url to a local file. +func DownloadFile(filepath string, url string) error { + // Get the data + resp, err := http.Get(url) + if err != nil { + return err + } else if resp.StatusCode != 200 { + return errors.New("Response status code: " + strconv.Itoa(resp.StatusCode)) + } + defer func() { + _ = resp.Body.Close() + }() + // Create the file + out, err := os.Create(filepath) + if err != nil { + return err + } + defer func() { + _ = out.Close() + }() + // Write the body to file + _, err = io.Copy(out, resp.Body) + return err +}