Codec selection (#892)

* Query for installed codecs

* Start modeling out codecs

* Can now specify a codec and get the correct settings returned from the model

* Return codecs in admin/serverconfig

* Start handling transcoding errors and return messages to user

* filter available codecs against a whitelist

* Fix merge

* Codecs are working

* Switching between codecs work

* Add apis for setting a custom video codec

* Cleanup the logging of transcoder errors

* Add v4l codec

* Add fetching v4l

* Add support for per-codec presets

* Use updated nvenc encoding parameters

* Update log message

* Some more codec WIP

* Turn off v4l. It is a mess.

* Try to make the lowest latency level a bit more playable

* Use a human redable display name in console messages

* Turn on transcoder persistent connections

* Add more codec-related user-facing error messages

* Give the initial offline state transcoder an id

* Force a minimum segment count of 3

* Disable qsv for now. set x264 specific params in VariantFlags

* Close body in case

* Ignore vbv underflow message, it is not actionable

* Determine a dynamic gop value based on the length of segments

* Add codec-specific tests

* Cleanup

* Ignore goconst lint warnings in codec file

* Troubleshoot omx

* Add more codec tests

* Remove no longer accurate comment

* Bundle admin from codec branch

* Revert back to old setting

* Cleanup list of codecs a bit

* Remove old references to the encoder preset

* Commit updated API documentation

* Update admin bundle

* Commit updated API documentation

* Add codec setting to api spec

* Commit updated API documentation

Co-authored-by: Owncast <owncast@owncast.online>
This commit is contained in:
Gabe Kangas
2021-04-15 13:55:51 -07:00
committed by GitHub
parent 7dec4fe063
commit 5214d81264
21 changed files with 845 additions and 180 deletions

373
core/transcoder/codecs.go Normal file
View File

@@ -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{}
}
}

View File

@@ -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()

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

81
core/transcoder/utils.go Normal file
View File

@@ -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
}