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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 845 additions and 180 deletions

View file

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

View file

@ -53,8 +53,8 @@ func GetDefaults() Defaults {
{
IsAudioPassthrough: true,
VideoBitrate: 1200,
EncoderPreset: "veryfast",
Framerate: 24,
CPUUsageLevel: 2,
},
},
}

View file

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

View file

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

View file

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

View file

@ -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() == "" {

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
}

File diff suppressed because one or more lines are too long

View file

@ -12,7 +12,7 @@ func GetLatencyConfigs() map[int]LatencyLevel {
return map[int]LatencyLevel{
1: {Level: 1, SecondsPerSegment: 1, SegmentCount: 2},
2: {Level: 2, SecondsPerSegment: 2, SegmentCount: 2},
3: {Level: 3, SecondsPerSegment: 3, SegmentCount: 3},
3: {Level: 3, SecondsPerSegment: 2, SegmentCount: 3},
4: {Level: 4, SecondsPerSegment: 3, SegmentCount: 4}, // Default
5: {Level: 5, SecondsPerSegment: 4, SegmentCount: 5},
6: {Level: 6, SecondsPerSegment: 6, SegmentCount: 10},

View file

@ -25,8 +25,7 @@ type StreamOutputVariant struct {
ScaledWidth int `yaml:"scaledWidth" json:"scaledWidth,omitempty"`
ScaledHeight int `yaml:"scaledHeight" json:"scaledHeight,omitempty"`
Framerate int `yaml:"framerate" json:"framerate"`
EncoderPreset string `yaml:"encoderPreset" json:"encoderPreset"` // Remove after migration is no longer used
Framerate int `yaml:"framerate" json:"framerate"`
// CPUUsageLevel represents a codec preset to configure CPU usage.
CPUUsageLevel int `json:"cpuUsageLevel"`
}
@ -44,32 +43,6 @@ func (q *StreamOutputVariant) GetFramerate() int {
return 24
}
// GetEncoderPreset returns the preset or default.
func (q *StreamOutputVariant) GetEncoderPreset() string {
if q.IsVideoPassthrough {
return ""
}
if q.EncoderPreset != "" {
return q.EncoderPreset
}
return "veryfast"
}
// GetCPUUsageLevel will return the libx264 codec encoder preset that maps to a level.
func (q *StreamOutputVariant) GetCPUUsageLevel() int {
presetMapping := map[string]int{
"ultrafast": 1,
"superfast": 2,
"veryfast": 3,
"faster": 4,
"fast": 5,
}
return presetMapping[q.GetEncoderPreset()]
}
// GetIsAudioPassthrough will return if this variant audio is passthrough.
func (q *StreamOutputVariant) GetIsAudioPassthrough() bool {
if q.IsAudioPassthrough {

View file

@ -182,9 +182,9 @@ components:
framerate:
type: integer
description: The target frames per second of the video.
encoderPreset:
type: string
description: "The [H.264 preset value](https://trac.ffmpeg.org/wiki/Encode/H.264) selected for this HLS variant."
cpuUsageLevel:
type: integer
description: "The amount of hardware utilization selected for this HLS variant."
TimestampedValue:
type: object
@ -984,13 +984,35 @@ paths:
- framerate: 30
videoPassthrough: false
videoBitrate: 1800
encoderPreset: veryfast
cpuUsageLevel: 2
audioPassthrough: true
- framerate: 24
videoPassthrough: false
videoBitrate: 1000
encoderPreset: superfast
audioPassthrough: true
cpuUsageLevel: 3
audioPassthrough: true
/api/admin/config/video/codec:
post:
summary: Set the video codec.
description: Sets the specific video codec that will be used for video encoding. Some codecs will support hardware acceleration. Not all codecs will be supported for all systems.
tags: ["Admin"]
security:
- AdminBasicAuth: []
responses:
'200':
$ref: "#/components/responses/BasicResponse"
requestBody:
content:
application/json:
schema:
type: object
properties:
value:
description: The video codec to change to.
type: string
example:
value: libx264
/api/admin/config/s3:
post:

File diff suppressed because one or more lines are too long

View file

@ -115,6 +115,9 @@ func Start() error {
// Disable chat
http.HandleFunc("/api/admin/config/chat/disable", middleware.RequireAdminAuth(admin.SetChatDisabled))
// Set video codec
http.HandleFunc("/api/admin/config/video/codec", middleware.RequireAdminAuth(admin.SetVideoCodec))
// Return all webhooks
http.HandleFunc("/api/admin/webhooks", middleware.RequireAdminAuth(admin.GetWebhooks))