transcoder.go 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. package transcoder
  2. /*
  3. Transcoder.go
  4. This module handle real-time transcoding of media files
  5. that is not supported by playing on web.
  6. */
  7. import (
  8. "io"
  9. "log"
  10. "net/http"
  11. "os/exec"
  12. "time"
  13. )
  14. type TranscodeOutputResolution string
  15. const (
  16. TranscodeResolution_360p TranscodeOutputResolution = "360p"
  17. TranscodeResolution_720p TranscodeOutputResolution = "720p"
  18. TranscodeResolution_1080p TranscodeOutputResolution = "1280p"
  19. TranscodeResolution_original TranscodeOutputResolution = ""
  20. )
  21. // Transcode and stream the given file. Make sure ffmpeg is installed before calling to transcoder.
  22. func TranscodeAndStream(w http.ResponseWriter, r *http.Request, inputFile string, resolution TranscodeOutputResolution) {
  23. // Build the FFmpeg command based on the resolution parameter
  24. var cmd *exec.Cmd
  25. transcodeFormatArgs := []string{"-f", "mp4", "-vcodec", "libx264", "-preset", "superfast", "-g", "60", "-movflags", "frag_keyframe+empty_moov+faststart", "pipe:1"}
  26. var args []string
  27. switch resolution {
  28. case "360p":
  29. args = append([]string{"-i", inputFile, "-vf", "scale=-1:360"}, transcodeFormatArgs...)
  30. case "720p":
  31. args = append([]string{"-i", inputFile, "-vf", "scale=-1:720"}, transcodeFormatArgs...)
  32. case "1080p":
  33. args = append([]string{"-i", inputFile, "-vf", "scale=-1:1080"}, transcodeFormatArgs...)
  34. case "":
  35. // Original resolution
  36. args = append([]string{"-i", inputFile}, transcodeFormatArgs...)
  37. default:
  38. http.Error(w, "Invalid resolution parameter", http.StatusBadRequest)
  39. return
  40. }
  41. cmd = exec.Command("ffmpeg", args...)
  42. // Set response headers for streaming MP4 video
  43. w.Header().Set("Content-Type", "video/mp4")
  44. w.Header().Set("Transfer-Encoding", "chunked")
  45. w.Header().Set("Cache-Control", "public, max-age=3600, s-maxage=3600, must-revalidate")
  46. w.Header().Set("Accept-Ranges", "bytes")
  47. // Get the command output pipe
  48. stdout, err := cmd.StdoutPipe()
  49. if err != nil {
  50. http.Error(w, "Failed to create output pipe", http.StatusInternalServerError)
  51. return
  52. }
  53. // Get the command error pipe to capture standard error
  54. stderr, err := cmd.StderrPipe()
  55. if err != nil {
  56. http.Error(w, "Failed to create error pipe", http.StatusInternalServerError)
  57. log.Printf("Failed to create error pipe: %v", err)
  58. return
  59. }
  60. // Start the command
  61. if err := cmd.Start(); err != nil {
  62. http.Error(w, "Failed to start FFmpeg", http.StatusInternalServerError)
  63. return
  64. }
  65. // Create a channel to signal when the client disconnects
  66. done := make(chan struct{})
  67. // Monitor client connection close
  68. go func() {
  69. <-r.Context().Done()
  70. time.Sleep(300 * time.Millisecond)
  71. cmd.Process.Kill() // Kill the FFmpeg process when client disconnects
  72. done <- struct{}{}
  73. //close(done)
  74. }()
  75. // Copy the command output to the HTTP response in a separate goroutine
  76. go func() {
  77. if _, err := io.Copy(w, stdout); err != nil {
  78. // End of video or client disconnected
  79. cmd.Process.Kill()
  80. return
  81. }
  82. }()
  83. // Read and log the command standard error
  84. go func() {
  85. errOutput, _ := io.ReadAll(stderr)
  86. if len(errOutput) > 0 {
  87. log.Printf("FFmpeg error output: %s", string(errOutput))
  88. }
  89. }()
  90. go func() {
  91. if err := cmd.Wait(); err != nil {
  92. log.Printf("FFmpeg process exited: %v", err)
  93. return
  94. }
  95. }()
  96. // Wait for the command to finish or client disconnect
  97. <-done
  98. log.Println("[Media Server] Transcode client disconnected")
  99. }