Download recordings from BigBlueButton
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

280 lines
7.4 KiB

  1. package main
  2. import (
  3. "bufio"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "io/ioutil"
  8. "log"
  9. "math"
  10. "net/http"
  11. "os"
  12. "os/exec"
  13. "path"
  14. "strconv"
  15. "strings"
  16. )
  17. func main() {
  18. fmt.Println("BigBlueButton video creator/downloader")
  19. scanner := bufio.NewScanner(os.Stdin)
  20. fmt.Print("Enter URL of recording: ")
  21. scanner.Scan()
  22. presentationUrl := scanner.Text()
  23. result := strings.SplitAfter(presentationUrl, "?meetingId=")
  24. presentationId := result[1]
  25. fmt.Print("Name of recording: ")
  26. scanner.Scan()
  27. meetingName := scanner.Text()
  28. result2 := strings.Split(result[0], "/playback/")
  29. baseUrl := result2[0] + "/presentation/" + presentationId
  30. fmt.Println("baseUrl= ", baseUrl)
  31. //read duration of recording and meeting name from meta.xml
  32. metaUrl := baseUrl + "/metadata.xml"
  33. responseMeta, err := http.Get(metaUrl)
  34. if err != nil {
  35. log.Fatal(err)
  36. }
  37. defer func() {
  38. _ = responseMeta.Body.Close()
  39. }()
  40. metaBody, err := ioutil.ReadAll(responseMeta.Body)
  41. if err != nil {
  42. log.Fatal(err)
  43. }
  44. //finding correct duration-ending of last slide
  45. timeString := strings.SplitAfter(string(metaBody), "<duration>")
  46. duration := strings.Split(timeString[1], "</duration>")
  47. fmt.Println("duration of recording=", duration[0], "ms")
  48. shapesUrl := baseUrl + "/shapes.svg"
  49. webcamsWebmUrl := baseUrl + "/video/webcams.webm"
  50. webcamsMp4Url := baseUrl + "/video/webcams.mp4"
  51. deskshareWebmUrl := baseUrl + "/deskshare/deskshare.webm"
  52. deskshareMp4Url := baseUrl + "/deskshare/deskshare.mp4"
  53. //read content of the shapes.svg file, and assign shapes to it
  54. responseShapes, err := http.Get(shapesUrl)
  55. if err != nil {
  56. log.Fatal(err)
  57. }
  58. defer func() {
  59. _ = responseShapes.Body.Close()
  60. }()
  61. shapesBody, err := ioutil.ReadAll(responseShapes.Body)
  62. if err != nil {
  63. log.Fatal(err)
  64. }
  65. shapes := string(shapesBody)
  66. fmt.Println("creating directory: ", presentationId)
  67. if _, err := os.Stat(presentationId); os.IsNotExist(err) {
  68. err = os.Mkdir(presentationId, 0700)
  69. if err != nil {
  70. fmt.Println("Failed to create temporary folder:", err.Error())
  71. }
  72. }
  73. // Find and print slide timings, image Urls
  74. durations := make(map[int]float64)
  75. vidnames := make(map[int]string)
  76. imgnames := make(map[int]string)
  77. inValue, outValue, truncated := 0.0, 0.0, 0.0
  78. inSrc, outSrc, pngSrc := "0.0", "10.5", "presentation/"
  79. i := 1 // number of png pictures for slide
  80. //parse for in= out= href= from /shapes.svg
  81. ins := strings.Split(shapes, "in=\"")
  82. outs := strings.Split(shapes, "out=\"")
  83. pngs := strings.Split(shapes, "xlink:href=\"")
  84. for k := 1; k < len(ins); k++ {
  85. intext := strings.SplitAfter(ins[k], "\"")
  86. realin := strings.Split(intext[0], "\"")
  87. inSrc = realin[0]
  88. outtext := strings.SplitAfter(outs[k], "\"")
  89. realout := strings.Split(outtext[0], "\"")
  90. outSrc = realout[0]
  91. imgtext := strings.SplitAfter(pngs[k], "\"")
  92. realpng := strings.Split(imgtext[0], "\"")
  93. pngSrc = realpng[0]
  94. inValue, _ = strconv.ParseFloat(inSrc, 64)
  95. outValue, _ = strconv.ParseFloat(outSrc, 64)
  96. truncated = (outValue*10 - inValue*10) / 10
  97. durations[i] = truncated
  98. imgnames[i] = "s" + strconv.Itoa(i) + ".png"
  99. vidnames[i] = "v" + strconv.Itoa(i) + ".mp4"
  100. imgUrl := baseUrl + "/" + pngSrc
  101. if err := DownloadFile(path.Join(presentationId+"/"+imgnames[i]), imgUrl); err != nil {
  102. panic(err)
  103. }
  104. i++
  105. }
  106. //correct duration of last slide
  107. outValue, _ = strconv.ParseFloat(duration[0], 64)
  108. outValue = outValue / 1000
  109. outValue = math.Round(outValue*100) / 100
  110. fmt.Println("ending of presentation=", outValue)
  111. truncated = (outValue*10 - inValue*10) / 10
  112. durations[i-1] = math.Round(truncated*100) / 100
  113. fmt.Println("Duration of last slide according to meta.xml is: ", durations[i-1])
  114. // create mp4 files from png files
  115. fmt.Println("Creating videos from slide pictures, duration is given as seconds")
  116. for j := 1; j < i; j++ {
  117. fmt.Println(imgnames[j], " ", vidnames[j], " ", durations[j])
  118. cmd := exec.Command("ffmpeg",
  119. "-loop", "1",
  120. "-framerate", "1",
  121. "-i", path.Join(presentationId, imgnames[j]),
  122. "-t", fmt.Sprint(durations[j]),
  123. "-pix_fmt", "yuv420p",
  124. "-vf", "scale=-1:1080",
  125. path.Join(presentationId, vidnames[j]))
  126. err = cmd.Run()
  127. if err != nil {
  128. fmt.Println("Failed to create video from slide:", err.Error())
  129. return
  130. }
  131. }
  132. //create video_list.txt file to cancat with ffmpeg
  133. f, err := os.Create("video_list.txt")
  134. if err != nil {
  135. fmt.Println(err)
  136. return
  137. }
  138. for j := 1; j < i; j++ {
  139. _, err := f.WriteString("file " + path.Join(presentationId, vidnames[j]) + "\n")
  140. if err != nil {
  141. fmt.Println(err)
  142. _ = f.Close()
  143. return
  144. }
  145. }
  146. err = f.Close()
  147. if err != nil {
  148. fmt.Println(err)
  149. return
  150. }
  151. // concat slide videos to create one piece of video file: slides.mp4
  152. fmt.Println("merging slide videos to create: slides.mp4")
  153. cmd := exec.Command("ffmpeg",
  154. "-f", "concat",
  155. "-safe", "0",
  156. "-i", "video_list.txt",
  157. "-c", "copy",
  158. path.Join(presentationId, "slides.mp4"))
  159. err = cmd.Run()
  160. if err != nil {
  161. fmt.Println("Failed to merge slide videos:", err.Error())
  162. return
  163. }
  164. fmt.Println("slide videos merged")
  165. // download webcams
  166. webcamsFile := ""
  167. fmt.Println("downloading webcams")
  168. err = DownloadFile(path.Join(presentationId, "webcams.webm"), webcamsWebmUrl)
  169. if err != nil {
  170. err = DownloadFile(path.Join(presentationId, "webcams.mp4"), webcamsMp4Url)
  171. if err != nil {
  172. fmt.Println("No webcams")
  173. } else {
  174. fmt.Println("webcams.mp4 file is downloaded")
  175. webcamsFile = path.Join(presentationId, "webcams.mp4")
  176. }
  177. } else {
  178. fmt.Println("webcams.webm file is downloaded")
  179. webcamsFile = path.Join(presentationId, "webcams.webm")
  180. }
  181. // download deskshare
  182. deskshareFile := ""
  183. fmt.Println("downloading deskshare")
  184. err = DownloadFile(path.Join(presentationId, "deskshare.webm"), deskshareWebmUrl)
  185. if err != nil {
  186. err = DownloadFile(path.Join(presentationId, "deskshare.mp4"), deskshareMp4Url)
  187. if err != nil {
  188. fmt.Println("No deskshare")
  189. } else {
  190. fmt.Println("deskshare.mp4 file is downloaded")
  191. deskshareFile = path.Join(presentationId, "deskshare.mp4")
  192. }
  193. } else {
  194. fmt.Println("deskshare.webm file is downloaded")
  195. deskshareFile = path.Join(presentationId, "deskshare.webm")
  196. }
  197. fmt.Println("merging slides, webcams and deskshare side by side")
  198. streams := 1
  199. cmd = exec.Command("ffmpeg", "-i", path.Join(presentationId, "slides.mp4"))
  200. if webcamsFile != "" {
  201. cmd.Args = append(cmd.Args, "-i")
  202. cmd.Args = append(cmd.Args, webcamsFile)
  203. streams++
  204. }
  205. if deskshareFile != "" {
  206. cmd.Args = append(cmd.Args, "-i")
  207. cmd.Args = append(cmd.Args, deskshareFile)
  208. streams++
  209. }
  210. cmd.Args = append(cmd.Args, "-c")
  211. cmd.Args = append(cmd.Args, "copy")
  212. for i := 0; i < streams; i++ {
  213. cmd.Args = append(cmd.Args, "-map")
  214. cmd.Args = append(cmd.Args, strconv.Itoa(i))
  215. }
  216. cmd.Args = append(cmd.Args, meetingName+".mkv")
  217. err = cmd.Run()
  218. if err != nil {
  219. fmt.Println("Failed to merge slides and webcams:", err.Error())
  220. return
  221. }
  222. fmt.Println("Name of the final video is: ", meetingName)
  223. _ = os.RemoveAll(presentationId + "/")
  224. _ = os.Remove("video_list.txt")
  225. }
  226. // DownloadFile will download a url to a local file.
  227. func DownloadFile(filepath string, url string) error {
  228. // Get the data
  229. resp, err := http.Get(url)
  230. if err != nil {
  231. return err
  232. } else if resp.StatusCode != 200 {
  233. return errors.New("Response status code: " + strconv.Itoa(resp.StatusCode))
  234. }
  235. defer func() {
  236. _ = resp.Body.Close()
  237. }()
  238. // Create the file
  239. out, err := os.Create(filepath)
  240. if err != nil {
  241. return err
  242. }
  243. defer func() {
  244. _ = out.Close()
  245. }()
  246. // Write the body to file
  247. _, err = io.Copy(out, resp.Body)
  248. return err
  249. }