owncast/ffmpeg.go

197 lines
7 KiB
Go
Raw Normal View History

2020-05-30 04:08:33 +03:00
package main
import (
"fmt"
2020-06-15 07:17:01 +03:00
"math"
2020-05-30 04:08:33 +03:00
"os"
"os/exec"
2020-06-02 02:53:31 +03:00
"path"
2020-06-13 00:14:05 +03:00
"strconv"
2020-06-09 11:52:15 +03:00
"strings"
2020-06-18 02:29:47 +03:00
log "github.com/sirupsen/logrus"
2020-05-30 04:08:33 +03:00
)
func showStreamOfflineState(configuration Config) {
fmt.Println("----- Stream offline! Showing offline state!")
2020-06-02 02:53:31 +03:00
var outputDir = configuration.PublicHLSPath
2020-06-09 11:52:15 +03:00
var variantPlaylistPath = configuration.PublicHLSPath
2020-05-30 04:08:33 +03:00
2020-06-03 11:34:05 +03:00
if configuration.IPFS.Enabled || configuration.S3.Enabled {
2020-06-02 02:53:31 +03:00
outputDir = configuration.PrivateHLSPath
2020-06-09 11:52:15 +03:00
variantPlaylistPath = configuration.PrivateHLSPath
2020-06-02 02:53:31 +03:00
}
2020-06-09 11:52:15 +03:00
outputDir = path.Join(outputDir, "%v")
var variantPlaylistName = path.Join(variantPlaylistPath, "%v", "stream.m3u8")
var videoMaps = make([]string, 0)
var streamMaps = make([]string, 0)
var videoMapsString = ""
var streamMappingString = ""
if configuration.VideoSettings.EnablePassthrough || len(configuration.VideoSettings.StreamQualities) == 0 {
fmt.Println("Enabling passthrough video")
videoMapsString = "-b:v 1200k -b:a 128k" // Since we're compositing multiple sources we can't infer bitrate, so pick something reasonable.
streamMaps = append(streamMaps, fmt.Sprintf("v:%d", 0))
} else {
for index, quality := range configuration.VideoSettings.StreamQualities {
maxRate := math.Floor(float64(quality.Bitrate) * 0.8)
videoMaps = append(videoMaps, fmt.Sprintf("-map v:0 -c:v:%d libx264 -b:v:%d %dk -maxrate %dk -bufsize %dk", index, index, int(quality.Bitrate), int(maxRate), int(maxRate)))
streamMaps = append(streamMaps, fmt.Sprintf("v:%d", index))
videoMapsString = strings.Join(videoMaps, " ")
}
}
framerate := 25
streamMappingString = "-var_stream_map \"" + strings.Join(streamMaps, " ") + "\""
ffmpegFlags := []string{
"-hide_banner",
// "-stream_loop 100",
// "-fflags", "+genpts",
"-i", configuration.VideoSettings.OfflineImage,
"-i", "webroot/thumbnail.jpg",
"-filter_complex", "\"[0:v]scale=2640:2360[bg];[bg][1:v]overlay=200:250:enable='between(t,0,3)'\"",
videoMapsString, // All the different video variants
"-f hls",
// "-hls_list_size " + strconv.Itoa(configuration.Files.MaxNumberInPlaylist),
"-hls_time 4", // + strconv.Itoa(configuration.VideoSettings.ChunkLengthInSeconds),
"-hls_playlist_type", "event",
"-master_pl_name", "stream.m3u8",
"-strftime 1",
"-use_localtime 1",
"-hls_flags temp_file",
"-tune", "zerolatency",
"-g " + strconv.Itoa(framerate*2), " -keyint_min " + strconv.Itoa(framerate*2), // multiply your output frame rate * 2. For example, if your input is -framerate 30, then use -g 60
"-framerate " + strconv.Itoa(framerate),
"-preset " + configuration.VideoSettings.EncoderPreset,
"-sc_threshold 0", // don't create key frames on scene change - only according to -g
"-profile:v", "main", // Main for standard definition (SD) to 640×480, High for high definition (HD) to 1920×1080
// "-movflags +faststart",
"-pix_fmt yuv420p",
streamMappingString,
"-hls_segment_filename " + path.Join(outputDir, "offline-%s.ts"),
// "-s", "720x480", // size
variantPlaylistName,
}
ffmpegFlagsString := strings.Join(ffmpegFlags, " ")
ffmpegCmd := configuration.FFMpegPath + " " + ffmpegFlagsString
2020-06-19 01:31:51 +03:00
// fmt.Println(ffmpegCmd)
_, err := exec.Command("sh", "-c", ffmpegCmd).Output()
fmt.Println(err)
verifyError(err)
}
func startFfmpeg(configuration Config) {
var outputDir = configuration.PublicHLSPath
var variantPlaylistPath = configuration.PublicHLSPath
if configuration.IPFS.Enabled || configuration.S3.Enabled {
outputDir = configuration.PrivateHLSPath
variantPlaylistPath = configuration.PrivateHLSPath
}
outputDir = path.Join(outputDir, "%v")
var variantPlaylistName = path.Join(variantPlaylistPath, "%v", "stream.m3u8")
2020-06-09 11:52:15 +03:00
log.Printf("Starting transcoder saving to /%s.", variantPlaylistName)
pipePath := getTempPipePath()
2020-06-09 11:52:15 +03:00
var videoMaps = make([]string, 0)
var streamMaps = make([]string, 0)
var audioMaps = make([]string, 0)
var videoMapsString = ""
var audioMapsString = ""
var streamMappingString = ""
2020-06-19 07:57:02 +03:00
var profileString = ""
if configuration.VideoSettings.EnablePassthrough || len(configuration.VideoSettings.StreamQualities) == 0 {
fmt.Println("Enabling passthrough video")
streamMaps = append(streamMaps, fmt.Sprintf("v:%d,a:%d", 0, 0))
2020-06-19 07:57:02 +03:00
videoMaps = append(videoMaps, "-map v:0 -c:v copy")
videoMapsString = strings.Join(videoMaps, " ")
audioMaps = append(audioMaps, "-map a:0")
audioMapsString = strings.Join(audioMaps, " ") + " -c:a copy" // Pass through audio for all the variants, don't reencode
} else {
for index, quality := range configuration.VideoSettings.StreamQualities {
2020-06-15 07:17:01 +03:00
maxRate := math.Floor(float64(quality.Bitrate) * 0.8)
videoMaps = append(videoMaps, fmt.Sprintf("-map v:0 -c:v:%d libx264 -b:v:%d %dk -maxrate %dk -bufsize %dk", index, index, int(quality.Bitrate), int(maxRate), int(maxRate)))
streamMaps = append(streamMaps, fmt.Sprintf("v:%d,a:%d", index, index))
videoMapsString = strings.Join(videoMaps, " ")
audioMaps = append(audioMaps, "-map a:0")
audioMapsString = strings.Join(audioMaps, " ") + " -c:a copy" // Pass through audio for all the variants, don't reencode
2020-06-19 07:57:02 +03:00
profileString = "-profile:v high" // Main for standard definition (SD) to 640×480, High for high definition (HD) to 1920×1080
}
2020-06-09 11:52:15 +03:00
}
2020-06-15 08:26:31 +03:00
framerate := 25
2020-06-15 07:17:01 +03:00
streamMappingString = "-var_stream_map \"" + strings.Join(streamMaps, " ") + "\""
2020-06-09 11:52:15 +03:00
ffmpegFlags := []string{
"-hide_banner",
// "-re",
"-fflags", "+genpts",
2020-06-09 11:52:15 +03:00
"-i pipe:",
2020-06-12 03:34:20 +03:00
// "-vf scale=900:-2", // Re-enable in the future with a config to togging resizing?
// "-sws_flags fast_bilinear",
videoMapsString, // All the different video variants
audioMapsString,
2020-06-09 11:52:15 +03:00
"-master_pl_name stream.m3u8",
2020-06-15 07:17:01 +03:00
"-framerate " + strconv.Itoa(framerate),
"-g " + strconv.Itoa(framerate*2), " -keyint_min " + strconv.Itoa(framerate*2), // multiply your output frame rate * 2. For example, if your input is -framerate 30, then use -g 60
2020-06-14 04:00:26 +03:00
// "-r 25",
2020-06-09 23:15:44 +03:00
"-preset " + configuration.VideoSettings.EncoderPreset,
2020-06-12 03:34:20 +03:00
"-sc_threshold 0", // don't create key frames on scene change - only according to -g
2020-06-19 07:57:02 +03:00
profileString,
2020-06-14 04:00:26 +03:00
"-movflags +faststart",
"-pix_fmt yuv420p",
2020-06-09 11:52:15 +03:00
"-f hls",
"-hls_list_size " + strconv.Itoa(configuration.Files.MaxNumberInPlaylist),
"-hls_delete_threshold 10", // Keep 10 unreferenced segments on disk before they're deleted.
2020-06-13 00:14:05 +03:00
"-hls_time " + strconv.Itoa(configuration.VideoSettings.ChunkLengthInSeconds),
2020-06-09 11:52:15 +03:00
"-strftime 1",
"-use_localtime 1",
"-hls_playlist_type event",
"-hls_segment_filename " + path.Join(outputDir, "stream-%Y%m%d-%s.ts"),
"-hls_flags delete_segments+program_date_time+temp_file",
"-tune zerolatency",
// "-s", "720x480", // size
streamMappingString,
2020-06-09 11:52:15 +03:00
variantPlaylistName,
}
ffmpegFlagsString := strings.Join(ffmpegFlags, " ")
ffmpegCmd := "cat " + pipePath + " | " + configuration.FFMpegPath + " " + ffmpegFlagsString
2020-06-17 05:08:34 +03:00
// fmt.Println(ffmpegCmd)
2020-06-10 04:28:07 +03:00
_, err := exec.Command("sh", "-c", ffmpegCmd).Output()
2020-06-09 11:52:15 +03:00
fmt.Println(err)
2020-05-30 04:08:33 +03:00
verifyError(err)
}
func writePlaylist(data string, filePath string) {
f, err := os.Create(filePath)
defer f.Close()
2020-05-30 04:08:33 +03:00
if err != nil {
fmt.Println(err)
return
}
_, err = f.WriteString(data)
if err != nil {
fmt.Println(err)
return
}
}