|
|
@ -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>") |
|
|
|
duration := strings.Split(timeString[1], "</duration>") |
|
|
|
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 |
|
|
|
} |