thumbnailGenerator.go 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. package transcoder
  2. import (
  3. "io/ioutil"
  4. "os"
  5. "os/exec"
  6. "path"
  7. "strconv"
  8. "strings"
  9. "time"
  10. log "github.com/sirupsen/logrus"
  11. "github.com/owncast/owncast/config"
  12. "github.com/owncast/owncast/core/data"
  13. "github.com/owncast/owncast/utils"
  14. )
  15. var _timer *time.Ticker
  16. // StopThumbnailGenerator will stop the periodic generating of a thumbnail from video.
  17. func StopThumbnailGenerator() {
  18. if _timer != nil {
  19. _timer.Stop()
  20. }
  21. }
  22. // StartThumbnailGenerator starts generating thumbnails.
  23. func StartThumbnailGenerator(chunkPath string, variantIndex int) {
  24. // Every 20 seconds create a thumbnail from the most
  25. // recent video segment.
  26. _timer = time.NewTicker(20 * time.Second)
  27. quit := make(chan struct{})
  28. go func() {
  29. for {
  30. select {
  31. case <-_timer.C:
  32. if err := fireThumbnailGenerator(chunkPath, variantIndex); err != nil {
  33. log.Errorln("Unable to generate thumbnail:", err)
  34. }
  35. case <-quit:
  36. log.Debug("thumbnail generator has stopped")
  37. _timer.Stop()
  38. return
  39. }
  40. }
  41. }()
  42. }
  43. func fireThumbnailGenerator(segmentPath string, variantIndex int) error {
  44. // JPG takes less time to encode than PNG
  45. outputFile := path.Join(config.WebRoot, "thumbnail.jpg")
  46. previewGifFile := path.Join(config.WebRoot, "preview.gif")
  47. framePath := path.Join(segmentPath, strconv.Itoa(variantIndex))
  48. files, err := ioutil.ReadDir(framePath)
  49. if err != nil {
  50. return err
  51. }
  52. var modTime time.Time
  53. var names []string
  54. for _, fi := range files {
  55. if path.Ext(fi.Name()) != ".ts" {
  56. continue
  57. }
  58. if fi.Mode().IsRegular() {
  59. if !fi.ModTime().Before(modTime) {
  60. if fi.ModTime().After(modTime) {
  61. modTime = fi.ModTime()
  62. names = names[:0]
  63. }
  64. names = append(names, fi.Name())
  65. }
  66. }
  67. }
  68. if len(names) == 0 {
  69. return nil
  70. }
  71. mostRecentFile := path.Join(framePath, names[0])
  72. ffmpegPath := utils.ValidatedFfmpegPath(data.GetFfMpegPath())
  73. outputFileTemp := path.Join(config.WebRoot, "tempthumbnail.jpg")
  74. thumbnailCmdFlags := []string{
  75. ffmpegPath,
  76. "-y", // Overwrite file
  77. "-threads 1", // Low priority processing
  78. "-t 1", // Pull from frame 1
  79. "-i", mostRecentFile, // Input
  80. "-f image2", // format
  81. "-vframes 1", // Single frame
  82. outputFileTemp,
  83. }
  84. ffmpegCmd := strings.Join(thumbnailCmdFlags, " ")
  85. if _, err := exec.Command("sh", "-c", ffmpegCmd).Output(); err != nil {
  86. return err
  87. }
  88. // rename temp file
  89. if err := os.Rename(outputFileTemp, outputFile); err != nil {
  90. log.Errorln(err)
  91. }
  92. // If YP support is enabled also create an animated GIF preview
  93. if data.GetDirectoryEnabled() {
  94. makeAnimatedGifPreview(mostRecentFile, previewGifFile)
  95. }
  96. return nil
  97. }
  98. func makeAnimatedGifPreview(sourceFile string, outputFile string) {
  99. ffmpegPath := utils.ValidatedFfmpegPath(data.GetFfMpegPath())
  100. outputFileTemp := path.Join(config.WebRoot, "temppreview.gif")
  101. // Filter is pulled from https://engineering.giphy.com/how-to-make-gifs-with-ffmpeg/
  102. animatedGifFlags := []string{
  103. ffmpegPath,
  104. "-y", // Overwrite file
  105. "-threads 1", // Low priority processing
  106. "-i", sourceFile, // Input
  107. "-t 1", // Output is one second in length
  108. "-filter_complex", "\"[0:v] fps=8,scale=w=480:h=-1:flags=lanczos,split [a][b];[a] palettegen=stats_mode=full [p];[b][p] paletteuse=new=1\"",
  109. outputFileTemp,
  110. }
  111. ffmpegCmd := strings.Join(animatedGifFlags, " ")
  112. if _, err := exec.Command("sh", "-c", ffmpegCmd).Output(); err != nil {
  113. log.Errorln(err)
  114. // rename temp file
  115. } else if err := os.Rename(outputFileTemp, outputFile); err != nil {
  116. log.Errorln(err)
  117. }
  118. }