Initial commit
このコミットが含まれているのは:
コミット
751b779d8e
|
@ -0,0 +1,3 @@
|
|||
/.idea
|
||||
bbb-download
|
||||
*.mp4
|
|
@ -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.
|
|
@ -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
|
||||
}
|
新しいイシューから参照