thumbnailGenerator.go 3.7 KB

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