thumbnailGenerator.go 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  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) {
  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. log.Errorln("Unable to generate thumbnail:", err)
  33. }
  34. case <-quit:
  35. log.Debug("thumbnail generator has stopped")
  36. _timer.Stop()
  37. return
  38. }
  39. }
  40. }()
  41. }
  42. func fireThumbnailGenerator(segmentPath string, variantIndex int) error {
  43. // JPG takes less time to encode than PNG
  44. outputFile := path.Join(config.TempDir, "thumbnail.jpg")
  45. previewGifFile := path.Join(config.TempDir, "preview.gif")
  46. framePath := path.Join(segmentPath, strconv.Itoa(variantIndex))
  47. files, err := os.ReadDir(framePath)
  48. if err != nil {
  49. return err
  50. }
  51. var modTime time.Time
  52. var names []string
  53. for _, f := range files {
  54. if path.Ext(f.Name()) != ".ts" {
  55. continue
  56. }
  57. fi, err := f.Info()
  58. if err != nil {
  59. continue
  60. }
  61. if fi.Mode().IsRegular() {
  62. if !fi.ModTime().Before(modTime) {
  63. if fi.ModTime().After(modTime) {
  64. modTime = fi.ModTime()
  65. names = names[:0]
  66. }
  67. names = append(names, fi.Name())
  68. }
  69. }
  70. }
  71. if len(names) == 0 {
  72. return nil
  73. }
  74. mostRecentFile := path.Join(framePath, names[0])
  75. ffmpegPath := utils.ValidatedFfmpegPath(data.GetFfMpegPath())
  76. outputFileTemp := path.Join(config.TempDir, "tempthumbnail.jpg")
  77. thumbnailCmdFlags := []string{
  78. ffmpegPath,
  79. "-y", // Overwrite file
  80. "-threads 1", // Low priority processing
  81. "-t 1", // Pull from frame 1
  82. "-i", mostRecentFile, // Input
  83. "-f image2", // format
  84. "-vframes 1", // Single frame
  85. outputFileTemp,
  86. }
  87. ffmpegCmd := strings.Join(thumbnailCmdFlags, " ")
  88. if _, err := exec.Command("sh", "-c", ffmpegCmd).Output(); err != nil {
  89. return err
  90. }
  91. // rename temp file
  92. if err := utils.Move(outputFileTemp, outputFile); err != nil {
  93. log.Errorln(err)
  94. }
  95. makeAnimatedGifPreview(mostRecentFile, previewGifFile)
  96. return nil
  97. }
  98. func makeAnimatedGifPreview(sourceFile string, outputFile string) {
  99. ffmpegPath := utils.ValidatedFfmpegPath(data.GetFfMpegPath())
  100. outputFileTemp := path.Join(config.TempDir, "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 := utils.Move(outputFileTemp, outputFile); err != nil {
  116. log.Errorln(err)
  117. }
  118. }