diff --git a/build/admin/bundleAdmin.sh b/build/admin/bundleAdmin.sh
index ba01fcefa..f215cb68b 100755
--- a/build/admin/bundleAdmin.sh
+++ b/build/admin/bundleAdmin.sh
@@ -18,6 +18,7 @@ trap shutdown INT TERM ABRT EXIT
echo "Cloning owncast admin into $INSTALL_TEMP_DIRECTORY..."
git clone https://github.com/owncast/owncast-admin 2> /dev/null
cd owncast-admin
+git checkout gek/codec-selection
echo "Installing npm modules for the owncast admin..."
npm --silent install 2> /dev/null
diff --git a/config/defaults.go b/config/defaults.go
index bb5fc5635..0570ef604 100644
--- a/config/defaults.go
+++ b/config/defaults.go
@@ -53,8 +53,8 @@ func GetDefaults() Defaults {
{
IsAudioPassthrough: true,
VideoBitrate: 1200,
- EncoderPreset: "veryfast",
Framerate: 24,
+ CPUUsageLevel: 2,
},
},
}
diff --git a/controllers/admin/config.go b/controllers/admin/config.go
index 4aff48704..a57fc5cb1 100644
--- a/controllers/admin/config.go
+++ b/controllers/admin/config.go
@@ -438,26 +438,6 @@ func SetStreamOutputVariants(w http.ResponseWriter, r *http.Request) {
return
}
- // Temporary: Convert the cpuUsageLevel to a preset. In the future we will have
- // different codec models that will handle this for us and we won't
- // be keeping track of presets at all. But for now...
- presetMapping := []string{
- "ultrafast",
- "superfast",
- "veryfast",
- "faster",
- "fast",
- }
-
- for i, variant := range videoVariants.Value {
- preset := "superfast"
- if variant.CPUUsageLevel > 0 && variant.CPUUsageLevel <= len(presetMapping) {
- preset = presetMapping[variant.CPUUsageLevel-1]
- }
- variant.EncoderPreset = preset
- videoVariants.Value[i] = variant
- }
-
if err := data.SetStreamOutputVariants(videoVariants.Value); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update video config with provided values "+err.Error())
return
@@ -508,6 +488,26 @@ func SetChatDisabled(w http.ResponseWriter, r *http.Request) {
controllers.WriteSimpleResponse(w, true, "chat disabled status updated")
}
+// SetVideoCodec will change the codec used for video encoding.
+func SetVideoCodec(w http.ResponseWriter, r *http.Request) {
+ if !requirePOST(w, r) {
+ return
+ }
+
+ configValue, success := getValueFromRequest(w, r)
+ if !success {
+ controllers.WriteSimpleResponse(w, false, "unable to change video codec")
+ return
+ }
+
+ if err := data.SetVideoCodec(configValue.Value.(string)); err != nil {
+ controllers.WriteSimpleResponse(w, false, "unable to update codec")
+ return
+ }
+
+ controllers.WriteSimpleResponse(w, true, "video codec updated")
+}
+
// SetExternalActions will set the 3rd party actions for the web interface.
func SetExternalActions(w http.ResponseWriter, r *http.Request) {
type externalActionsRequest struct {
diff --git a/controllers/admin/serverConfig.go b/controllers/admin/serverConfig.go
index 042d41a39..9ac37d458 100644
--- a/controllers/admin/serverConfig.go
+++ b/controllers/admin/serverConfig.go
@@ -6,6 +6,7 @@ import (
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/data"
+ "github.com/owncast/owncast/core/transcoder"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
@@ -13,6 +14,8 @@ import (
// GetServerConfig gets the config details of the server.
func GetServerConfig(w http.ResponseWriter, r *http.Request) {
+ ffmpeg := utils.ValidatedFfmpegPath(data.GetFfMpegPath())
+
var videoQualityVariants = make([]models.StreamOutputVariant, 0)
for _, variant := range data.GetStreamOutputVariants() {
videoQualityVariants = append(videoQualityVariants, models.StreamOutputVariant{
@@ -20,10 +23,9 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
IsAudioPassthrough: variant.GetIsAudioPassthrough(),
IsVideoPassthrough: variant.IsVideoPassthrough,
Framerate: variant.GetFramerate(),
- EncoderPreset: variant.GetEncoderPreset(),
VideoBitrate: variant.VideoBitrate,
AudioBitrate: variant.AudioBitrate,
- CPUUsageLevel: variant.GetCPUUsageLevel(),
+ CPUUsageLevel: variant.CPUUsageLevel,
ScaledWidth: variant.ScaledWidth,
ScaledHeight: variant.ScaledHeight,
})
@@ -41,7 +43,7 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
NSFW: data.GetNSFW(),
CustomStyles: data.GetCustomStyles(),
},
- FFmpegPath: utils.ValidatedFfmpegPath(data.GetFfMpegPath()),
+ FFmpegPath: ffmpeg,
StreamKey: data.GetStreamKey(),
WebServerPort: config.WebServerPort,
RTMPServerPort: data.GetRTMPPortNumber(),
@@ -56,6 +58,8 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
},
S3: data.GetS3Config(),
ExternalActions: data.GetExternalActions(),
+ SupportedCodecs: transcoder.GetCodecs(ffmpeg),
+ VideoCodec: data.GetVideoCodec(),
}
w.Header().Set("Content-Type", "application/json")
@@ -77,6 +81,8 @@ type serverConfigAdminResponse struct {
YP yp `json:"yp"`
ChatDisabled bool `json:"chatDisabled"`
ExternalActions []models.ExternalAction `json:"externalActions"`
+ SupportedCodecs []string `json:"supportedCodecs"`
+ VideoCodec string `json:"videoCodec"`
}
type videoSettings struct {
diff --git a/core/core.go b/core/core.go
index 3d3010b9c..953a9cdbb 100644
--- a/core/core.go
+++ b/core/core.go
@@ -99,6 +99,7 @@ func transitionToOfflineVideoStreamContent() {
offlineFilePath := "static/" + offlineFilename
_transcoder := transcoder.NewTranscoder()
_transcoder.SetInput(offlineFilePath)
+ _transcoder.SetIdentifier("offline")
_transcoder.Start()
// Copy the logo to be the thumbnail
diff --git a/core/data/config.go b/core/data/config.go
index d1f4d2b9b..40f6680e7 100644
--- a/core/data/config.go
+++ b/core/data/config.go
@@ -39,6 +39,7 @@ const videoStreamOutputVariantsKey = "video_stream_output_variants"
const chatDisabledKey = "chat_disabled"
const externalActionsKey = "external_actions"
const customStylesKey = "custom_styles"
+const videoCodecKey = "video_codec"
// GetExtraPageBodyContent will return the user-supplied body content.
func GetExtraPageBodyContent() string {
@@ -481,6 +482,20 @@ func GetCustomStyles() string {
return style
}
+// SetVideoCodec will set the codec used for video encoding.
+func SetVideoCodec(codec string) error {
+ return _datastore.SetString(videoCodecKey, codec)
+}
+
+func GetVideoCodec() string {
+ codec, err := _datastore.GetString(videoCodecKey)
+ if codec == "" || err != nil {
+ return "libx264" // Default value
+ }
+
+ return codec
+}
+
// VerifySettings will perform a sanity check for specific settings values.
func VerifySettings() error {
if GetStreamKey() == "" {
diff --git a/core/transcoder/codecs.go b/core/transcoder/codecs.go
new file mode 100644
index 000000000..dcfa6a50a
--- /dev/null
+++ b/core/transcoder/codecs.go
@@ -0,0 +1,373 @@
+//nolint:goconst
+
+package transcoder
+
+import (
+ "fmt"
+ "os/exec"
+ "strings"
+
+ log "github.com/sirupsen/logrus"
+)
+
+// Codec represents a supported codec on the system.
+type Codec interface {
+ Name() string
+ DisplayName() string
+ GlobalFlags() string
+ PixelFormat() string
+ ExtraArguments() string
+ ExtraFilters() string
+ VariantFlags(v *HLSVariant) string
+ GetPresetForLevel(l int) string
+}
+
+var supportedCodecs = map[string]string{
+ (&Libx264Codec{}).Name(): "libx264",
+ (&OmxCodec{}).Name(): "omx",
+ (&VaapiCodec{}).Name(): "vaapi",
+ (&NvencCodec{}).Name(): "NVIDIA nvenc",
+}
+
+type Libx264Codec struct {
+}
+
+func (c *Libx264Codec) Name() string {
+ return "libx264"
+}
+
+func (c *Libx264Codec) DisplayName() string {
+ return "x264"
+}
+
+func (c *Libx264Codec) GlobalFlags() string {
+ return ""
+}
+
+func (c *Libx264Codec) PixelFormat() string {
+ return "yuv420p"
+}
+
+func (c *Libx264Codec) ExtraArguments() string {
+ return strings.Join([]string{
+ "-tune", "zerolatency", // Option used for good for fast encoding and low-latency streaming (always includes iframes in each segment)
+ }, " ")
+}
+
+func (c *Libx264Codec) ExtraFilters() string {
+ return ""
+}
+
+func (c *Libx264Codec) VariantFlags(v *HLSVariant) string {
+ bufferSize := int(float64(v.videoBitrate) * 1.2) // How often it checks the bitrate of encoded segments to see if it's too high/low.
+
+ return strings.Join([]string{
+ fmt.Sprintf("-x264-params:v:%d \"scenecut=0:open_gop=0\"", v.index), // How often the encoder checks the bitrate in order to meet average/max values
+ fmt.Sprintf("-bufsize:v:%d %dk", v.index, bufferSize),
+ fmt.Sprintf("-profile:v:%d %s", v.index, "high"), // Encoding profile
+ }, " ")
+}
+
+func (c *Libx264Codec) GetPresetForLevel(l int) string {
+ presetMapping := []string{
+ "ultrafast",
+ "superfast",
+ "veryfast",
+ "faster",
+ "fast",
+ }
+
+ if l >= len(presetMapping) {
+ return "superfast"
+ }
+
+ return presetMapping[l]
+}
+
+type OmxCodec struct {
+}
+
+func (c *OmxCodec) Name() string {
+ return "h264_omx"
+}
+
+func (c *OmxCodec) DisplayName() string {
+ return "OpenMAX (omx)"
+}
+
+func (c *OmxCodec) GlobalFlags() string {
+ return ""
+}
+
+func (c *OmxCodec) PixelFormat() string {
+ return "yuv420p"
+}
+
+func (c *OmxCodec) ExtraArguments() string {
+ return strings.Join([]string{
+ "-tune", "zerolatency", // Option used for good for fast encoding and low-latency streaming (always includes iframes in each segment)
+ }, " ")
+}
+
+func (c *OmxCodec) ExtraFilters() string {
+ return ""
+}
+
+func (c *OmxCodec) VariantFlags(v *HLSVariant) string {
+ return ""
+}
+
+func (c *OmxCodec) GetPresetForLevel(l int) string {
+ presetMapping := []string{
+ "ultrafast",
+ "superfast",
+ "veryfast",
+ "faster",
+ "fast",
+ }
+
+ if l >= len(presetMapping) {
+ return "superfast"
+ }
+
+ return presetMapping[l]
+}
+
+type VaapiCodec struct {
+}
+
+func (c *VaapiCodec) Name() string {
+ return "h264_vaapi"
+}
+
+func (c *VaapiCodec) DisplayName() string {
+ return "VA-API"
+}
+
+func (c *VaapiCodec) GlobalFlags() string {
+ flags := []string{
+ "-vaapi_device", "/dev/dri/renderD128",
+ }
+
+ return strings.Join(flags, " ")
+}
+
+func (c *VaapiCodec) PixelFormat() string {
+ return "vaapi_vld"
+}
+
+func (c *VaapiCodec) ExtraFilters() string {
+ return "format=nv12,hwupload"
+}
+
+func (c *VaapiCodec) ExtraArguments() string {
+ return ""
+}
+
+func (c *VaapiCodec) VariantFlags(v *HLSVariant) string {
+ return ""
+}
+
+func (c *VaapiCodec) GetPresetForLevel(l int) string {
+ presetMapping := []string{
+ "ultrafast",
+ "superfast",
+ "veryfast",
+ "faster",
+ "fast",
+ }
+
+ if l >= len(presetMapping) {
+ return "superfast"
+ }
+
+ return presetMapping[l]
+}
+
+type NvencCodec struct {
+}
+
+func (c *NvencCodec) Name() string {
+ return "h264_nvenc"
+}
+
+func (c *NvencCodec) DisplayName() string {
+ return "nvidia nvenc"
+}
+
+func (c *NvencCodec) GlobalFlags() string {
+ flags := []string{
+ "-hwaccel cuda",
+ }
+
+ return strings.Join(flags, " ")
+}
+
+func (c *NvencCodec) PixelFormat() string {
+ return "yuv420p"
+}
+
+func (c *NvencCodec) ExtraArguments() string {
+ return ""
+}
+
+func (c *NvencCodec) ExtraFilters() string {
+ return ""
+}
+
+func (c *NvencCodec) VariantFlags(v *HLSVariant) string {
+ tuning := "ll" // low latency
+ return fmt.Sprintf("-tune:v:%d %s", v.index, tuning)
+}
+
+func (c *NvencCodec) GetPresetForLevel(l int) string {
+ presetMapping := []string{
+ "p1",
+ "p2",
+ "p3",
+ "p4",
+ "p5",
+ }
+
+ if l >= len(presetMapping) {
+ return "p3"
+ }
+
+ return presetMapping[l]
+}
+
+type QuicksyncCodec struct {
+}
+
+func (c *QuicksyncCodec) Name() string {
+ return "h264_qsv"
+}
+
+func (c *QuicksyncCodec) DisplayName() string {
+ return "Intel QuickSync"
+}
+
+func (c *QuicksyncCodec) GlobalFlags() string {
+ return ""
+}
+
+func (c *QuicksyncCodec) PixelFormat() string {
+ return "nv12"
+}
+
+func (c *QuicksyncCodec) ExtraArguments() string {
+ return ""
+}
+
+func (c *QuicksyncCodec) ExtraFilters() string {
+ return ""
+}
+
+func (c *QuicksyncCodec) VariantFlags(v *HLSVariant) string {
+ return ""
+}
+
+func (c *QuicksyncCodec) GetPresetForLevel(l int) string {
+ presetMapping := []string{
+ "ultrafast",
+ "superfast",
+ "veryfast",
+ "faster",
+ "fast",
+ }
+
+ if l >= len(presetMapping) {
+ return "superfast"
+ }
+
+ return presetMapping[l]
+}
+
+type Video4Linux struct{}
+
+func (c *Video4Linux) Name() string {
+ return "h264_v4l2m2m"
+}
+
+func (c *Video4Linux) DisplayName() string {
+ return "Video4Linux"
+}
+
+func (c *Video4Linux) GlobalFlags() string {
+ return ""
+}
+
+func (c *Video4Linux) PixelFormat() string {
+ return "nv21"
+}
+
+func (c *Video4Linux) ExtraArguments() string {
+ return ""
+}
+
+func (c *Video4Linux) ExtraFilters() string {
+ return ""
+}
+
+func (c *Video4Linux) VariantFlags(v *HLSVariant) string {
+ return ""
+}
+
+func (c *Video4Linux) GetPresetForLevel(l int) string {
+ presetMapping := []string{
+ "ultrafast",
+ "superfast",
+ "veryfast",
+ "faster",
+ "fast",
+ }
+
+ if l >= len(presetMapping) {
+ return "superfast"
+ }
+
+ return presetMapping[l]
+}
+
+// GetCodecs will return the supported codecs available on the system.
+func GetCodecs(ffmpegPath string) []string {
+ codecs := make([]string, 0)
+
+ cmd := exec.Command(ffmpegPath, "-encoders")
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ log.Errorln(err)
+ return codecs
+ }
+
+ response := string(out)
+ lines := strings.Split(response, "\n")
+ for _, line := range lines {
+ if strings.Contains(line, "H.264") {
+ fields := strings.Fields(line)
+ codec := fields[1]
+ if _, supported := supportedCodecs[codec]; supported {
+ codecs = append(codecs, codec)
+ }
+ }
+ }
+
+ return codecs
+}
+
+func getCodec(name string) Codec {
+ switch name {
+ case (&NvencCodec{}).Name():
+ return &NvencCodec{}
+ case (&VaapiCodec{}).Name():
+ return &VaapiCodec{}
+ case (&QuicksyncCodec{}).Name():
+ return &QuicksyncCodec{}
+ case (&OmxCodec{}).Name():
+ return &OmxCodec{}
+ case (&Video4Linux{}).Name():
+ return &Video4Linux{}
+ default:
+ return &Libx264Codec{}
+ }
+}
diff --git a/core/transcoder/fileWriterReceiverService.go b/core/transcoder/fileWriterReceiverService.go
index b12941dde..c6b2059a2 100644
--- a/core/transcoder/fileWriterReceiverService.go
+++ b/core/transcoder/fileWriterReceiverService.go
@@ -62,6 +62,8 @@ func (s *FileWriterReceiverService) uploadHandler(w http.ResponseWriter, r *http
writePath := filepath.Join(config.PrivateHLSStoragePath, path)
var buf bytes.Buffer
+ defer r.Body.Close()
+
_, _ = io.Copy(&buf, r.Body)
data := buf.Bytes()
diff --git a/core/transcoder/transcoder.go b/core/transcoder/transcoder.go
index 57cc457d9..f55607395 100644
--- a/core/transcoder/transcoder.go
+++ b/core/transcoder/transcoder.go
@@ -1,6 +1,7 @@
package transcoder
import (
+ "bufio"
"fmt"
"os/exec"
"strconv"
@@ -27,6 +28,7 @@ type Transcoder struct {
ffmpegPath string
segmentIdentifier string
internalListenerPort string
+ codec Codec
currentStreamOutputSettings []models.StreamOutputVariant
currentLatencyLevel models.LatencyLevel
@@ -46,7 +48,7 @@ type HLSVariant struct {
audioBitrate string // The audio bitrate
isAudioPassthrough bool // Override all settings and just copy the audio stream
- encoderPreset string // A collection of automatic settings for the encoder. https://trac.ffmpeg.org/wiki/Encode/H.264#crf
+ cpuUsageLevel int // The amount of hardware to use for encoding a stream
}
// VideoSize is the scaled size of the video output.
@@ -81,25 +83,43 @@ func (t *Transcoder) Stop() {
// Start will execute the transcoding process with the settings previously set.
func (t *Transcoder) Start() {
- command := t.getString()
+ _lastTranscoderLogMessage = ""
- log.Tracef("Video transcoder started with %d stream variants.", len(t.variants))
+ command := t.getString()
+ log.Infof("Video transcoder started using %s with %d stream variants.", t.codec.DisplayName(), len(t.variants))
if config.EnableDebugFeatures {
log.Println(command)
}
_commandExec = exec.Command("sh", "-c", command)
- err := _commandExec.Start()
+ stdout, err := _commandExec.StderrPipe()
+ if err != nil {
+ panic(err)
+ }
+
+ err = _commandExec.Start()
if err != nil {
log.Errorln("Transcoder error. See transcoder.log for full output to debug.")
log.Panicln(err, command)
}
+ go func() {
+ scanner := bufio.NewScanner(stdout)
+ for scanner.Scan() {
+ line := scanner.Text()
+ handleTranscoderMessage(line)
+ }
+ }()
+
err = _commandExec.Wait()
if t.TranscoderCompleted != nil {
t.TranscoderCompleted(err)
}
+
+ if err != nil {
+ log.Errorln("transcoding error. look at transcoder.log to help debug. your copy of ffmpeg may not support your selected codec of", t.codec.Name(), "https://owncast.online/docs/troubleshooting/#codecs")
+ }
}
func (t *Transcoder) getString() string {
@@ -121,9 +141,12 @@ func (t *Transcoder) getString() string {
hlsOptionsString = "-hls_flags " + strings.Join(hlsOptionFlags, "+")
}
ffmpegFlags := []string{
+ `FFREPORT=file="transcoder.log":level=32`,
t.ffmpegPath,
"-hide_banner",
"-loglevel warning",
+ t.codec.GlobalFlags(),
+ "-fflags +genpts", // Generate presentation time stamp if missing
"-i ", t.input,
t.getVariantsString(),
@@ -133,12 +156,12 @@ func (t *Transcoder) getString() string {
"-hls_time", strconv.Itoa(t.currentLatencyLevel.SecondsPerSegment), // Length of each segment
"-hls_list_size", strconv.Itoa(t.currentLatencyLevel.SegmentCount), // Max # in variant playlist
- "-hls_delete_threshold", "10", // Start deleting files after hls_list_size + 10
hlsOptionsString,
+ "-segment_format_options", "mpegts_flags=+initial_discontinuity:mpegts_copyts=1",
// Video settings
- "-tune", "zerolatency", // Option used for good for fast encoding and low-latency streaming (always includes iframes in each segment)
- "-pix_fmt", "yuv420p", // Force yuv420p color format
+ t.codec.ExtraArguments(),
+ "-pix_fmt", t.codec.PixelFormat(),
"-sc_threshold", "0", // Disable scene change detection for creating segments
// Filenames
@@ -149,9 +172,7 @@ func (t *Transcoder) getString() string {
"-max_muxing_queue_size", "400", // Workaround for Too many packets error: https://trac.ffmpeg.org/ticket/6375?cversion=0
"-method PUT -http_persistent 0", // HLS results sent back to us will be over PUTs
- "-fflags +genpts", // Generate presentation time stamp if missing
localListenerAddress + "/%v/stream.m3u8", // Send HLS playlists back to us over HTTP
- "2> transcoder.log", // Log to a file for debugging
}
return strings.Join(ffmpegFlags, " ")
@@ -181,7 +202,7 @@ func getVariantFromConfigQuality(quality models.StreamOutputVariant, index int)
// Set a default, reasonable preset if one is not provided.
// "superfast" and "ultrafast" are generally not recommended since they look bad.
// https://trac.ffmpeg.org/wiki/Encode/H.264
- variant.encoderPreset = quality.GetEncoderPreset()
+ variant.cpuUsageLevel = quality.CPUUsageLevel
variant.SetVideoBitrate(quality.VideoBitrate)
variant.SetAudioBitrate(strconv.Itoa(quality.AudioBitrate) + "k")
@@ -202,6 +223,7 @@ func NewTranscoder() *Transcoder {
transcoder.currentStreamOutputSettings = data.GetStreamOutputVariants()
transcoder.currentLatencyLevel = data.GetStreamLatencyLevel()
+ transcoder.codec = getCodec(data.GetVideoCodec())
var outputPath string
if data.GetS3Config().Enabled {
@@ -233,12 +255,25 @@ func (v *HLSVariant) getVariantString(t *Transcoder) string {
v.getAudioQualityString(),
}
- if v.videoSize.Width != 0 || v.videoSize.Height != 0 {
- variantEncoderCommands = append(variantEncoderCommands, v.getScalingString())
+ if (v.videoSize.Width != 0 || v.videoSize.Height != 0) && !v.isVideoPassthrough {
+ // Order here matters, you must scale before changing hardware formats
+ filters := []string{
+ v.getScalingString(),
+ }
+ if t.codec.ExtraFilters() != "" {
+ filters = append(filters, t.codec.ExtraFilters())
+ }
+ scalingAlgorithm := "bilinear"
+ filterString := fmt.Sprintf("-sws_flags %s -filter:v:%d \"%s\"", scalingAlgorithm, v.index, strings.Join(filters, ","))
+ variantEncoderCommands = append(variantEncoderCommands, filterString)
+ } else if t.codec.ExtraFilters() != "" && !v.isVideoPassthrough {
+ filterString := fmt.Sprintf("-filter:v:%d \"%s\"", v.index, t.codec.ExtraFilters())
+ variantEncoderCommands = append(variantEncoderCommands, filterString)
}
- if v.encoderPreset != "" {
- variantEncoderCommands = append(variantEncoderCommands, fmt.Sprintf("-preset %s", v.encoderPreset))
+ preset := t.codec.GetPresetForLevel(v.cpuUsageLevel)
+ if preset != "" {
+ variantEncoderCommands = append(variantEncoderCommands, fmt.Sprintf("-preset %s", preset))
}
return strings.Join(variantEncoderCommands, " ")
@@ -276,8 +311,7 @@ func (v *HLSVariant) SetVideoScalingHeight(height int) {
}
func (v *HLSVariant) getScalingString() string {
- scalingAlgorithm := "bilinear"
- return fmt.Sprintf("-filter:v:%d \"scale=%s\" -sws_flags %s", v.index, v.videoSize.getString(), scalingAlgorithm)
+ return fmt.Sprintf("scale=%s", v.videoSize.getString())
}
// Video Quality
@@ -292,11 +326,14 @@ func (v *HLSVariant) getVideoQualityString(t *Transcoder) string {
return fmt.Sprintf("-map v:0 -c:v:%d copy", v.index)
}
- encoderCodec := "libx264"
-
- // -1 to work around segments being generated slightly larger than expected.
- // https://trac.ffmpeg.org/ticket/6915?replyto=58#comment:57
- gop := (t.currentLatencyLevel.SecondsPerSegment * v.framerate) - 1
+ // Determine if we should force key frames every 1, 2 or 3 frames.
+ isEven := t.currentLatencyLevel.SecondsPerSegment%2 == 0
+ gop := v.framerate * 2
+ if t.currentLatencyLevel.SecondsPerSegment == 1 {
+ gop = v.framerate
+ } else if !isEven {
+ gop = v.framerate * 3
+ }
// For limiting the output bitrate
// https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
@@ -304,18 +341,16 @@ func (v *HLSVariant) getVideoQualityString(t *Transcoder) string {
// Adjust the max & buffer size until the output bitrate doesn't exceed the ~+10% that Apple's media validator
// complains about.
maxBitrate := int(float64(v.videoBitrate) * 1.06) // Max is a ~+10% over specified bitrate.
- bufferSize := int(float64(v.videoBitrate) * 1.2) // How often it checks the bitrate of encoded segments to see if it's too high/low.
cmd := []string{
"-map v:0",
- fmt.Sprintf("-c:v:%d %s", v.index, encoderCodec), // Video codec used for this variant
+ fmt.Sprintf("-c:v:%d %s", v.index, t.codec.Name()), // Video codec used for this variant
fmt.Sprintf("-b:v:%d %dk", v.index, v.videoBitrate), // The average bitrate for this variant
fmt.Sprintf("-maxrate:v:%d %dk", v.index, maxBitrate), // The max bitrate allowed for this variant
- fmt.Sprintf("-bufsize:v:%d %dk", v.index, bufferSize), // How often the encoder checks the bitrate in order to meet average/max values
- fmt.Sprintf("-g:v:%d %d", v.index, gop), // How often i-frames are encoded into the segments
- fmt.Sprintf("-profile:v:%d %s", v.index, "high"), // Encoding profile
+ fmt.Sprintf("-g:v:%d %d", v.index, gop), // Suggested interval where i-frames are encoded into the segments
+ fmt.Sprintf("-keyint_min:v:%d %d", v.index, gop), // minimum i-keyframe interval
fmt.Sprintf("-r:v:%d %d", v.index, v.framerate),
- fmt.Sprintf("-x264-params:v:%d \"scenecut=0:open_gop=0:min-keyint=%d:keyint=%d\"", v.index, gop, gop), // How often i-frames are encoded into the segments
+ t.codec.VariantFlags(v),
}
return strings.Join(cmd, " ")
@@ -326,9 +361,9 @@ func (v *HLSVariant) SetVideoFramerate(framerate int) {
v.framerate = framerate
}
-// SetEncoderPreset will set the video encoder preset of this variant.
-func (v *HLSVariant) SetEncoderPreset(preset string) {
- v.encoderPreset = preset
+// SetCPUUsageLevel will set the hardware usage of this variant.
+func (v *HLSVariant) SetCPUUsageLevel(level int) {
+ v.cpuUsageLevel = level
}
// Audio Quality
@@ -377,3 +412,7 @@ func (t *Transcoder) SetIdentifier(output string) {
func (t *Transcoder) SetInternalHTTPPort(port string) {
t.internalListenerPort = port
}
+
+func (t *Transcoder) SetCodec(codecName string) {
+ t.codec = getCodec(codecName)
+}
diff --git a/core/transcoder/transcoder_nvenc_test.go b/core/transcoder/transcoder_nvenc_test.go
new file mode 100644
index 000000000..bcb49729f
--- /dev/null
+++ b/core/transcoder/transcoder_nvenc_test.go
@@ -0,0 +1,48 @@
+package transcoder
+
+import (
+ "testing"
+
+ "github.com/owncast/owncast/models"
+)
+
+func TestFFmpegNvencCommand(t *testing.T) {
+ latencyLevel := models.GetLatencyLevel(3)
+ codec := NvencCodec{}
+
+ transcoder := new(Transcoder)
+ transcoder.ffmpegPath = "/fake/path/ffmpeg"
+ transcoder.SetInput("fakecontent.flv")
+ transcoder.SetOutputPath("fakeOutput")
+ transcoder.SetIdentifier("jdoieGg")
+ transcoder.SetInternalHTTPPort("8123")
+ transcoder.SetCodec(codec.Name())
+ transcoder.currentLatencyLevel = latencyLevel
+
+ variant := HLSVariant{}
+ variant.videoBitrate = 1200
+ variant.isAudioPassthrough = true
+ variant.SetVideoFramerate(30)
+ variant.SetCPUUsageLevel(2)
+ transcoder.AddVariant(variant)
+
+ variant2 := HLSVariant{}
+ variant2.videoBitrate = 3500
+ variant2.isAudioPassthrough = true
+ variant2.SetVideoFramerate(24)
+ variant2.SetCPUUsageLevel(4)
+ transcoder.AddVariant(variant2)
+
+ variant3 := HLSVariant{}
+ variant3.isAudioPassthrough = true
+ variant3.isVideoPassthrough = true
+ transcoder.AddVariant(variant3)
+
+ cmd := transcoder.getString()
+
+ expected := `FFREPORT=file="transcoder.log":level=32 /fake/path/ffmpeg -hide_banner -loglevel warning -hwaccel cuda -fflags +genpts -i fakecontent.flv -map v:0 -c:v:0 h264_nvenc -b:v:0 1200k -maxrate:v:0 1272k -g:v:0 60 -keyint_min:v:0 60 -r:v:0 30 -tune:v:0 ll -map a:0? -c:a:0 copy -preset p3 -map v:0 -c:v:1 h264_nvenc -b:v:1 3500k -maxrate:v:1 3710k -g:v:1 48 -keyint_min:v:1 48 -r:v:1 24 -tune:v:1 ll -map a:0? -c:a:1 copy -preset p5 -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset p1 -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 2 -hls_list_size 3 -segment_format_options mpegts_flags=+initial_discontinuity:mpegts_copyts=1 -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -strftime 1 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdoieGg%s.ts -max_muxing_queue_size 400 -method PUT -http_persistent 0 http://127.0.0.1:8123/%v/stream.m3u8`
+
+ if cmd != expected {
+ t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
+ }
+}
diff --git a/core/transcoder/transcoder_omx_test.go b/core/transcoder/transcoder_omx_test.go
new file mode 100644
index 000000000..a734884e8
--- /dev/null
+++ b/core/transcoder/transcoder_omx_test.go
@@ -0,0 +1,48 @@
+package transcoder
+
+import (
+ "testing"
+
+ "github.com/owncast/owncast/models"
+)
+
+func TestFFmpegOmxCommand(t *testing.T) {
+ latencyLevel := models.GetLatencyLevel(3)
+ codec := OmxCodec{}
+
+ transcoder := new(Transcoder)
+ transcoder.ffmpegPath = "/fake/path/ffmpeg"
+ transcoder.SetInput("fakecontent.flv")
+ transcoder.SetOutputPath("fakeOutput")
+ transcoder.SetIdentifier("jdFsdfzGg")
+ transcoder.SetInternalHTTPPort("8123")
+ transcoder.SetCodec(codec.Name())
+ transcoder.currentLatencyLevel = latencyLevel
+
+ variant := HLSVariant{}
+ variant.videoBitrate = 1200
+ variant.isAudioPassthrough = true
+ variant.SetVideoFramerate(30)
+ variant.SetCPUUsageLevel(2)
+ transcoder.AddVariant(variant)
+
+ variant2 := HLSVariant{}
+ variant2.videoBitrate = 3500
+ variant2.isAudioPassthrough = true
+ variant2.SetVideoFramerate(24)
+ variant2.SetCPUUsageLevel(4)
+ transcoder.AddVariant(variant2)
+
+ variant3 := HLSVariant{}
+ variant3.isAudioPassthrough = true
+ variant3.isVideoPassthrough = true
+ transcoder.AddVariant(variant3)
+
+ cmd := transcoder.getString()
+
+ expected := `FFREPORT=file="transcoder.log":level=32 /fake/path/ffmpeg -hide_banner -loglevel warning -fflags +genpts -i fakecontent.flv -map v:0 -c:v:0 h264_omx -b:v:0 1200k -maxrate:v:0 1272k -g:v:0 60 -keyint_min:v:0 60 -r:v:0 30 -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 h264_omx -b:v:1 3500k -maxrate:v:1 3710k -g:v:1 48 -keyint_min:v:1 48 -r:v:1 24 -map a:0? -c:a:1 copy -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 2 -hls_list_size 3 -segment_format_options mpegts_flags=+initial_discontinuity:mpegts_copyts=1 -tune zerolatency -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -strftime 1 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdFsdfzGg%s.ts -max_muxing_queue_size 400 -method PUT -http_persistent 0 http://127.0.0.1:8123/%v/stream.m3u8`
+
+ if cmd != expected {
+ t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
+ }
+}
diff --git a/core/transcoder/transcoder_test.go b/core/transcoder/transcoder_test.go
deleted file mode 100644
index eec72ce13..000000000
--- a/core/transcoder/transcoder_test.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package transcoder
-
-import (
- "testing"
-
- "github.com/owncast/owncast/models"
-)
-
-func TestFFmpegCommand(t *testing.T) {
- latencyLevel := models.GetLatencyLevel(3)
-
- transcoder := new(Transcoder)
- transcoder.ffmpegPath = "/fake/path/ffmpeg"
- transcoder.SetInput("fakecontent.flv")
- transcoder.SetOutputPath("fakeOutput")
- transcoder.SetIdentifier("jdofFGg")
- transcoder.SetInternalHTTPPort("8123")
- transcoder.currentLatencyLevel = latencyLevel
-
- variant := HLSVariant{}
- variant.videoBitrate = 1200
- variant.isAudioPassthrough = true
- variant.encoderPreset = "veryfast"
- variant.SetVideoFramerate(30)
- transcoder.AddVariant(variant)
-
- variant2 := HLSVariant{}
- variant2.videoBitrate = 3500
- variant2.isAudioPassthrough = true
- variant2.encoderPreset = "faster"
- variant2.SetVideoFramerate(24)
- transcoder.AddVariant(variant2)
-
- variant3 := HLSVariant{}
- variant3.isAudioPassthrough = true
- variant3.isVideoPassthrough = true
- transcoder.AddVariant(variant3)
-
- cmd := transcoder.getString()
-
- expected := `/fake/path/ffmpeg -hide_banner -loglevel warning -i fakecontent.flv -map v:0 -c:v:0 libx264 -b:v:0 1200k -maxrate:v:0 1272k -bufsize:v:0 1440k -g:v:0 89 -profile:v:0 high -r:v:0 30 -x264-params:v:0 "scenecut=0:open_gop=0:min-keyint=89:keyint=89" -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 libx264 -b:v:1 3500k -maxrate:v:1 3710k -bufsize:v:1 4200k -g:v:1 71 -profile:v:1 high -r:v:1 24 -x264-params:v:1 "scenecut=0:open_gop=0:min-keyint=71:keyint=71" -map a:0? -c:a:1 copy -preset faster -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 3 -hls_list_size 3 -hls_delete_threshold 10 -tune zerolatency -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -strftime 1 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg%s.ts -max_muxing_queue_size 400 -method PUT -http_persistent 0 -fflags +genpts http://127.0.0.1:8123/%v/stream.m3u8 2> transcoder.log`
-
- if cmd != expected {
- t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
- }
-}
diff --git a/core/transcoder/transcoder_vaapi_test.go b/core/transcoder/transcoder_vaapi_test.go
new file mode 100644
index 000000000..24db257e9
--- /dev/null
+++ b/core/transcoder/transcoder_vaapi_test.go
@@ -0,0 +1,48 @@
+package transcoder
+
+import (
+ "testing"
+
+ "github.com/owncast/owncast/models"
+)
+
+func TestFFmpegVaapiCommand(t *testing.T) {
+ latencyLevel := models.GetLatencyLevel(3)
+ codec := VaapiCodec{}
+
+ transcoder := new(Transcoder)
+ transcoder.ffmpegPath = "/fake/path/ffmpeg"
+ transcoder.SetInput("fakecontent.flv")
+ transcoder.SetOutputPath("fakeOutput")
+ transcoder.SetIdentifier("jdofFGg")
+ transcoder.SetInternalHTTPPort("8123")
+ transcoder.SetCodec(codec.Name())
+ transcoder.currentLatencyLevel = latencyLevel
+
+ variant := HLSVariant{}
+ variant.videoBitrate = 1200
+ variant.isAudioPassthrough = true
+ variant.SetVideoFramerate(30)
+ variant.SetCPUUsageLevel(2)
+ transcoder.AddVariant(variant)
+
+ variant2 := HLSVariant{}
+ variant2.videoBitrate = 3500
+ variant2.isAudioPassthrough = true
+ variant2.SetVideoFramerate(24)
+ variant2.SetCPUUsageLevel(4)
+ transcoder.AddVariant(variant2)
+
+ variant3 := HLSVariant{}
+ variant3.isAudioPassthrough = true
+ variant3.isVideoPassthrough = true
+ transcoder.AddVariant(variant3)
+
+ cmd := transcoder.getString()
+
+ expected := `FFREPORT=file="transcoder.log":level=32 /fake/path/ffmpeg -hide_banner -loglevel warning -vaapi_device /dev/dri/renderD128 -fflags +genpts -i fakecontent.flv -map v:0 -c:v:0 h264_vaapi -b:v:0 1200k -maxrate:v:0 1272k -g:v:0 60 -keyint_min:v:0 60 -r:v:0 30 -map a:0? -c:a:0 copy -filter:v:0 "format=nv12,hwupload" -preset veryfast -map v:0 -c:v:1 h264_vaapi -b:v:1 3500k -maxrate:v:1 3710k -g:v:1 48 -keyint_min:v:1 48 -r:v:1 24 -map a:0? -c:a:1 copy -filter:v:1 "format=nv12,hwupload" -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 2 -hls_list_size 3 -segment_format_options mpegts_flags=+initial_discontinuity:mpegts_copyts=1 -pix_fmt vaapi_vld -sc_threshold 0 -master_pl_name stream.m3u8 -strftime 1 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg%s.ts -max_muxing_queue_size 400 -method PUT -http_persistent 0 http://127.0.0.1:8123/%v/stream.m3u8`
+
+ if cmd != expected {
+ t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
+ }
+}
diff --git a/core/transcoder/transcoder_x264_test.go b/core/transcoder/transcoder_x264_test.go
new file mode 100644
index 000000000..d5c42fea7
--- /dev/null
+++ b/core/transcoder/transcoder_x264_test.go
@@ -0,0 +1,48 @@
+package transcoder
+
+import (
+ "testing"
+
+ "github.com/owncast/owncast/models"
+)
+
+func TestFFmpegx264Command(t *testing.T) {
+ latencyLevel := models.GetLatencyLevel(3)
+ codec := Libx264Codec{}
+
+ transcoder := new(Transcoder)
+ transcoder.ffmpegPath = "/fake/path/ffmpeg"
+ transcoder.SetInput("fakecontent.flv")
+ transcoder.SetOutputPath("fakeOutput")
+ transcoder.SetIdentifier("jdofFGg")
+ transcoder.SetInternalHTTPPort("8123")
+ transcoder.SetCodec(codec.Name())
+ transcoder.currentLatencyLevel = latencyLevel
+
+ variant := HLSVariant{}
+ variant.videoBitrate = 1200
+ variant.isAudioPassthrough = true
+ variant.SetVideoFramerate(30)
+ variant.SetCPUUsageLevel(2)
+ transcoder.AddVariant(variant)
+
+ variant2 := HLSVariant{}
+ variant2.videoBitrate = 3500
+ variant2.isAudioPassthrough = true
+ variant2.SetVideoFramerate(24)
+ variant2.SetCPUUsageLevel(4)
+ transcoder.AddVariant(variant2)
+
+ variant3 := HLSVariant{}
+ variant3.isAudioPassthrough = true
+ variant3.isVideoPassthrough = true
+ transcoder.AddVariant(variant3)
+
+ cmd := transcoder.getString()
+
+ expected := `FFREPORT=file="transcoder.log":level=32 /fake/path/ffmpeg -hide_banner -loglevel warning -fflags +genpts -i fakecontent.flv -map v:0 -c:v:0 libx264 -b:v:0 1200k -maxrate:v:0 1272k -g:v:0 60 -keyint_min:v:0 60 -r:v:0 30 -x264-params:v:0 "scenecut=0:open_gop=0" -bufsize:v:0 1440k -profile:v:0 high -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 libx264 -b:v:1 3500k -maxrate:v:1 3710k -g:v:1 48 -keyint_min:v:1 48 -r:v:1 24 -x264-params:v:1 "scenecut=0:open_gop=0" -bufsize:v:1 4200k -profile:v:1 high -map a:0? -c:a:1 copy -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 2 -hls_list_size 3 -segment_format_options mpegts_flags=+initial_discontinuity:mpegts_copyts=1 -tune zerolatency -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -strftime 1 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg%s.ts -max_muxing_queue_size 400 -method PUT -http_persistent 0 http://127.0.0.1:8123/%v/stream.m3u8`
+
+ if cmd != expected {
+ t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
+ }
+}
diff --git a/core/transcoder/utils.go b/core/transcoder/utils.go
new file mode 100644
index 000000000..aba015468
--- /dev/null
+++ b/core/transcoder/utils.go
@@ -0,0 +1,81 @@
+package transcoder
+
+import (
+ "strings"
+ "sync"
+
+ log "github.com/sirupsen/logrus"
+)
+
+var _lastTranscoderLogMessage = ""
+var l = &sync.RWMutex{}
+
+var errorMap = map[string]string{
+ "Unrecognized option 'vaapi_device'": "you are likely trying to utilize a vaapi codec, but your version of ffmpeg or your hardware doesn't support it. change your codec to libx264 and restart your stream",
+ "unable to open display": "your copy of ffmpeg is likely installed via snap packages. please uninstall and re-install via a non-snap method. https://owncast.online/docs/troubleshooting/#misc-video-issues",
+ "Failed to open file 'http://127.0.0.1": "error transcoding. make sure your version of ffmpeg is compatible with your selected codec or is recent enough https://owncast.online/docs/troubleshooting/#codecs",
+ "can't configure encoder": "error with codec. if your copy of ffmpeg or your hardware does not support your selected codec you may need to select another",
+ "Unable to parse option value": "you are likely trying to utilize a specific codec, but your version of ffmpeg or your hardware doesn't support it. either fix your ffmpeg install or try changing your codec to libx264 and restart your stream",
+ "OpenEncodeSessionEx failed: out of memory": "your NVIDIA gpu is limiting the number of concurrent stream qualities you can support. remove a stream output variant and try again.",
+ "Cannot use rename on non file protocol, this may lead to races and temporary partial files": "",
+ "No VA display found for device": "vaapi not enabled. either your copy of ffmpeg does not support it, your hardware does not support it, or you need to install additional drivers for your hardware.",
+ "Could not find a valid device": "your codec is either not supported or not configured properly",
+ "H.264 bitstream error": "transcoding content error playback issues may arise. you may want to use the default codec if you are not already.",
+
+ `Unknown encoder 'h264_qsv'`: "your copy of ffmpeg does not have support for Intel QuickSync encoding (h264_qsv). change the selected codec in your video settings",
+ `Unknown encoder 'h264_vaapi'`: "your copy of ffmpeg does not have support for VA-API encoding (h264_vaapi). change the selected codec in your video settings",
+ `Unknown encoder 'h264_nvenc'`: "your copy of ffmpeg does not have support for NVIDIA hardware encoding (h264_nvenc). change the selected codec in your video settings",
+ `Unknown encoder 'h264_x264'`: "your copy of ffmpeg does not have support for the default x264 codec (h264_x264). download a version of ffmpeg that supports this.",
+ `Unrecognized option 'x264-params`: "your copy of ffmpeg does not have support for the default libx264 codec (h264_x264). download a version of ffmpeg that supports this.",
+
+ // Generic error for a codec
+ "Unrecognized option": "error with codec. if your copy of ffmpeg or your hardware does not support your selected codec you may need to select another",
+}
+
+var ignoredErrors = []string{
+ "Duplicated segment filename detected",
+ "Error while opening encoder for output stream",
+ "Unable to parse option value",
+ "Last message repeated",
+ "Option not found",
+ "use of closed network connection",
+ "URL read error: End of file",
+ "upload playlist failed, will retry with a new http session",
+ "VBV underflow",
+ "Cannot use rename on non file protocol",
+}
+
+func handleTranscoderMessage(message string) {
+ log.Debugln(message)
+
+ l.Lock()
+ defer l.Unlock()
+
+ // Ignore certain messages that we don't care about.
+ for _, error := range ignoredErrors {
+ if strings.Contains(message, error) {
+ return
+ }
+ }
+
+ // Convert specific transcoding messages to human-readable messages.
+ for error, displayMessage := range errorMap {
+ if strings.Contains(message, error) {
+ message = displayMessage
+ break
+ }
+ }
+
+ if message == "" {
+ return
+ }
+
+ // No good comes from a flood of repeated messages.
+ if message == _lastTranscoderLogMessage {
+ return
+ }
+
+ log.Error(message)
+
+ _lastTranscoderLogMessage = message
+}
diff --git a/doc/api/index.html b/doc/api/index.html
index 08625e2d9..3b73c7ce2 100644
--- a/doc/api/index.html
+++ b/doc/api/index.html
@@ -15,10 +15,10 @@