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 }