2021-02-19 08:05:52 +01:00
package transcoder
2020-06-26 02:44:47 +02:00
import (
2021-04-15 22:55:51 +02:00
"bufio"
2020-06-26 02:44:47 +02:00
"fmt"
2021-07-03 21:28:25 +02:00
"io"
2020-06-26 02:44:47 +02:00
"os/exec"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
2020-10-02 08:34:29 +02:00
"github.com/teris-io/shortid"
2020-06-26 02:44:47 +02:00
2020-10-05 19:07:09 +02:00
"github.com/owncast/owncast/config"
2021-02-19 08:05:52 +01:00
"github.com/owncast/owncast/core/data"
2021-05-23 04:25:33 +02:00
"github.com/owncast/owncast/logging"
2021-02-19 08:05:52 +01:00
"github.com/owncast/owncast/models"
2020-10-05 19:07:09 +02:00
"github.com/owncast/owncast/utils"
2020-06-26 02:44:47 +02:00
)
2020-07-12 01:00:23 +02:00
var _commandExec * exec . Cmd
2020-11-13 00:14:59 +01:00
// Transcoder is a single instance of a video transcoder.
2020-06-26 02:44:47 +02:00
type Transcoder struct {
input string
2021-07-03 21:28:25 +02:00
stdin * io . PipeReader
2020-06-26 02:44:47 +02:00
segmentOutputPath string
playlistOutputPath string
variants [ ] HLSVariant
appendToStream bool
2020-07-15 03:52:48 +02:00
ffmpegPath string
2020-10-02 08:34:29 +02:00
segmentIdentifier string
2021-02-19 08:05:52 +01:00
internalListenerPort string
2021-04-15 22:55:51 +02:00
codec Codec
2021-02-19 08:05:52 +01:00
currentStreamOutputSettings [ ] models . StreamOutputVariant
currentLatencyLevel models . LatencyLevel
TranscoderCompleted func ( error )
2020-06-26 02:44:47 +02:00
}
2020-11-13 00:14:59 +01:00
// HLSVariant is a combination of settings that results in a single HLS stream.
2020-06-26 02:44:47 +02:00
type HLSVariant struct {
index int
videoSize VideoSize // Resizes the video via scaling
framerate int // The output framerate
2020-08-06 21:19:35 +02:00
videoBitrate int // The output bitrate
2020-06-26 02:44:47 +02:00
isVideoPassthrough bool // Override all settings and just copy the video stream
audioBitrate string // The audio bitrate
isAudioPassthrough bool // Override all settings and just copy the audio stream
2021-04-15 22:55:51 +02:00
cpuUsageLevel int // The amount of hardware to use for encoding a stream
2020-06-26 02:44:47 +02:00
}
2020-11-13 00:14:59 +01:00
// VideoSize is the scaled size of the video output.
2020-06-26 02:44:47 +02:00
type VideoSize struct {
Width int
Height int
}
2020-11-13 00:14:59 +01:00
// getString returns a WxH formatted getString for scaling video output.
2020-06-26 02:44:47 +02:00
func ( v * VideoSize ) getString ( ) string {
widthString := strconv . Itoa ( v . Width )
heightString := strconv . Itoa ( v . Height )
if widthString != "0" && heightString != "0" {
return widthString + ":" + heightString
} else if widthString != "0" {
return widthString + ":-2"
} else if heightString != "0" {
return "-2:" + heightString
}
return ""
}
2020-07-12 01:00:23 +02:00
func ( t * Transcoder ) Stop ( ) {
2020-07-12 02:22:10 +02:00
log . Traceln ( "Transcoder STOP requested." )
2020-10-17 00:04:31 +02:00
err := _commandExec . Process . Kill ( )
if err != nil {
log . Errorln ( err )
2020-07-12 01:00:23 +02:00
}
}
2020-06-26 02:44:47 +02:00
// Start will execute the transcoding process with the settings previously set.
func ( t * Transcoder ) Start ( ) {
2021-04-15 22:55:51 +02:00
_lastTranscoderLogMessage = ""
2020-06-26 02:44:47 +02:00
2021-04-15 22:55:51 +02:00
command := t . getString ( )
log . Infof ( "Video transcoder started using %s with %d stream variants." , t . codec . DisplayName ( ) , len ( t . variants ) )
2021-04-16 06:34:51 +02:00
createVariantDirectories ( )
2020-06-26 02:44:47 +02:00
2021-02-19 08:05:52 +01:00
if config . EnableDebugFeatures {
2020-07-09 03:27:24 +02:00
log . Println ( command )
}
2020-07-12 01:00:23 +02:00
_commandExec = exec . Command ( "sh" , "-c" , command )
2021-07-03 21:28:25 +02:00
if t . stdin != nil {
2021-07-09 20:16:44 +02:00
_commandExec . Stdin = t . stdin
2021-07-03 21:28:25 +02:00
}
2021-04-15 22:55:51 +02:00
stdout , err := _commandExec . StderrPipe ( )
if err != nil {
panic ( err )
}
2021-07-09 20:16:44 +02:00
if err := _commandExec . Start ( ) ; err != nil {
2021-05-23 04:25:33 +02:00
log . Errorln ( "Transcoder error. See " , logging . GetTranscoderLogFilePath ( ) , " for full output to debug." )
2020-06-26 02:44:47 +02:00
log . Panicln ( err , command )
}
2021-04-15 22:55:51 +02:00
go func ( ) {
scanner := bufio . NewScanner ( stdout )
for scanner . Scan ( ) {
line := scanner . Text ( )
handleTranscoderMessage ( line )
}
} ( )
2020-10-14 23:07:38 +02:00
err = _commandExec . Wait ( )
if t . TranscoderCompleted != nil {
t . TranscoderCompleted ( err )
}
2021-04-15 22:55:51 +02:00
if err != nil {
2021-09-06 03:07:02 +02:00
log . Errorln ( "transcoding error. look at " , logging . GetTranscoderLogFilePath ( ) , " to help debug. your copy of ffmpeg may not support your selected codec of" , t . codec . Name ( ) , "https://owncast.online/docs/codecs/" )
2021-04-15 22:55:51 +02:00
}
2020-06-26 02:44:47 +02:00
}
func ( t * Transcoder ) getString ( ) string {
2021-02-19 08:05:52 +01:00
var port = t . internalListenerPort
localListenerAddress := "http://127.0.0.1:" + port
2020-10-14 23:07:38 +02:00
hlsOptionFlags := [ ] string { }
2020-06-26 02:44:47 +02:00
if t . appendToStream {
hlsOptionFlags = append ( hlsOptionFlags , "append_list" )
}
2020-10-02 08:34:29 +02:00
if t . segmentIdentifier == "" {
t . segmentIdentifier = shortid . MustGenerate ( )
}
2020-10-14 23:07:38 +02:00
hlsOptionsString := ""
if len ( hlsOptionFlags ) > 0 {
hlsOptionsString = "-hls_flags " + strings . Join ( hlsOptionFlags , "+" )
}
2020-06-26 02:44:47 +02:00
ffmpegFlags := [ ] string {
2021-05-23 04:25:33 +02:00
fmt . Sprintf ( ` FFREPORT=file="%s":level=32 ` , logging . GetTranscoderLogFilePath ( ) ) ,
2020-07-15 03:52:48 +02:00
t . ffmpegPath ,
2020-06-26 02:44:47 +02:00
"-hide_banner" ,
2020-10-14 23:07:38 +02:00
"-loglevel warning" ,
2021-04-15 22:55:51 +02:00
t . codec . GlobalFlags ( ) ,
"-fflags +genpts" , // Generate presentation time stamp if missing
2020-10-14 23:07:38 +02:00
"-i " , t . input ,
2020-06-26 02:44:47 +02:00
t . getVariantsString ( ) ,
// HLS Output
"-f" , "hls" ,
2020-10-14 23:07:38 +02:00
2021-02-19 08:05:52 +01:00
"-hls_time" , strconv . Itoa ( t . currentLatencyLevel . SecondsPerSegment ) , // Length of each segment
"-hls_list_size" , strconv . Itoa ( t . currentLatencyLevel . SegmentCount ) , // Max # in variant playlist
2020-10-14 23:07:38 +02:00
hlsOptionsString ,
2021-04-15 22:55:51 +02:00
"-segment_format_options" , "mpegts_flags=+initial_discontinuity:mpegts_copyts=1" ,
2020-06-26 02:44:47 +02:00
// Video settings
2021-04-15 22:55:51 +02:00
t . codec . ExtraArguments ( ) ,
"-pix_fmt" , t . codec . PixelFormat ( ) ,
2020-06-26 02:44:47 +02:00
"-sc_threshold" , "0" , // Disable scene change detection for creating segments
// Filenames
"-master_pl_name" , "stream.m3u8" ,
2020-10-14 23:07:38 +02:00
"-strftime 1" , // Support the use of strftime in filenames
"-hls_segment_filename" , localListenerAddress + "/%v/stream-" + t . segmentIdentifier + "%s.ts" , // Send HLS segments back to us over HTTP
2020-06-26 02:44:47 +02:00
"-max_muxing_queue_size" , "400" , // Workaround for Too many packets error: https://trac.ffmpeg.org/ticket/6375?cversion=0
2020-10-14 23:07:38 +02:00
2020-11-11 08:59:56 +01:00
"-method PUT -http_persistent 0" , // HLS results sent back to us will be over PUTs
2020-10-14 23:07:38 +02:00
localListenerAddress + "/%v/stream.m3u8" , // Send HLS playlists back to us over HTTP
2020-06-26 02:44:47 +02:00
}
return strings . Join ( ffmpegFlags , " " )
}
2021-02-19 08:05:52 +01:00
func getVariantFromConfigQuality ( quality models . StreamOutputVariant , index int ) HLSVariant {
2020-06-26 02:44:47 +02:00
variant := HLSVariant { }
variant . index = index
variant . isAudioPassthrough = quality . IsAudioPassthrough
variant . isVideoPassthrough = quality . IsVideoPassthrough
// If no audio bitrate is specified then we pass through original audio
if quality . AudioBitrate == 0 {
variant . isAudioPassthrough = true
}
if quality . VideoBitrate == 0 {
2020-11-12 23:23:52 +01:00
quality . VideoBitrate = 1200
2020-06-26 02:44:47 +02:00
}
// If the video is being passed through then
// don't continue to set options on the variant.
if variant . isVideoPassthrough {
return variant
}
// 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
2021-04-15 22:55:51 +02:00
variant . cpuUsageLevel = quality . CPUUsageLevel
2020-06-26 02:44:47 +02:00
2020-08-06 21:19:35 +02:00
variant . SetVideoBitrate ( quality . VideoBitrate )
2020-06-26 02:44:47 +02:00
variant . SetAudioBitrate ( strconv . Itoa ( quality . AudioBitrate ) + "k" )
variant . SetVideoScalingWidth ( quality . ScaledWidth )
variant . SetVideoScalingHeight ( quality . ScaledHeight )
2020-08-06 21:19:35 +02:00
variant . SetVideoFramerate ( quality . GetFramerate ( ) )
2020-06-26 02:44:47 +02:00
return variant
}
2020-11-13 00:14:59 +01:00
// NewTranscoder will return a new Transcoder, populated by the config.
2020-10-14 23:07:38 +02:00
func NewTranscoder ( ) * Transcoder {
2021-02-19 08:05:52 +01:00
ffmpegPath := utils . ValidatedFfmpegPath ( data . GetFfMpegPath ( ) )
2020-06-26 02:44:47 +02:00
transcoder := new ( Transcoder )
2021-02-19 08:05:52 +01:00
transcoder . ffmpegPath = ffmpegPath
transcoder . internalListenerPort = config . InternalHLSListenerPort
transcoder . currentStreamOutputSettings = data . GetStreamOutputVariants ( )
transcoder . currentLatencyLevel = data . GetStreamLatencyLevel ( )
2021-04-15 22:55:51 +02:00
transcoder . codec = getCodec ( data . GetVideoCodec ( ) )
2020-06-26 02:44:47 +02:00
var outputPath string
2021-02-19 08:05:52 +01:00
if data . GetS3Config ( ) . Enabled {
2020-06-26 02:44:47 +02:00
// Segments are not available via the local HTTP server
2020-10-03 23:35:03 +02:00
outputPath = config . PrivateHLSStoragePath
2020-06-26 02:44:47 +02:00
} else {
// Segments are available via the local HTTP server
2020-10-03 23:35:03 +02:00
outputPath = config . PublicHLSStoragePath
2020-06-26 02:44:47 +02:00
}
transcoder . segmentOutputPath = outputPath
// Playlists are available via the local HTTP server
2020-10-03 23:35:03 +02:00
transcoder . playlistOutputPath = config . PublicHLSStoragePath
2020-06-26 02:44:47 +02:00
2021-07-03 21:28:25 +02:00
transcoder . input = "pipe:0" // stdin
2020-06-26 02:44:47 +02:00
2021-02-19 08:05:52 +01:00
for index , quality := range transcoder . currentStreamOutputSettings {
2020-06-26 02:44:47 +02:00
variant := getVariantFromConfigQuality ( quality , index )
transcoder . AddVariant ( variant )
}
2020-10-14 23:07:38 +02:00
return transcoder
2020-06-26 02:44:47 +02:00
}
// Uses `map` https://www.ffmpeg.org/ffmpeg-all.html#Stream-specifiers-1 https://www.ffmpeg.org/ffmpeg-all.html#Advanced-options
2020-08-06 21:19:35 +02:00
func ( v * HLSVariant ) getVariantString ( t * Transcoder ) string {
2020-06-26 02:44:47 +02:00
variantEncoderCommands := [ ] string {
2020-08-06 21:19:35 +02:00
v . getVideoQualityString ( t ) ,
2020-06-26 02:44:47 +02:00
v . getAudioQualityString ( ) ,
}
2021-04-15 22:55:51 +02:00
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 )
2020-06-26 02:44:47 +02:00
}
2021-04-15 22:55:51 +02:00
preset := t . codec . GetPresetForLevel ( v . cpuUsageLevel )
if preset != "" {
variantEncoderCommands = append ( variantEncoderCommands , fmt . Sprintf ( "-preset %s" , preset ) )
2020-06-26 02:44:47 +02:00
}
return strings . Join ( variantEncoderCommands , " " )
}
2020-11-13 00:14:59 +01:00
// Get the command flags for the variants.
2020-06-26 02:44:47 +02:00
func ( t * Transcoder ) getVariantsString ( ) string {
var variantsCommandFlags = ""
var variantsStreamMaps = " -var_stream_map \""
for _ , variant := range t . variants {
2020-08-06 21:19:35 +02:00
variantsCommandFlags = variantsCommandFlags + " " + variant . getVariantString ( t )
2020-12-02 09:19:55 +01:00
singleVariantMap := ""
2021-02-19 08:05:52 +01:00
singleVariantMap = fmt . Sprintf ( "v:%d,a:%d " , variant . index , variant . index )
2020-12-02 09:19:55 +01:00
variantsStreamMaps = variantsStreamMaps + singleVariantMap
2020-06-26 02:44:47 +02:00
}
variantsCommandFlags = variantsCommandFlags + " " + variantsStreamMaps + "\""
return variantsCommandFlags
}
// Video Scaling
// https://trac.ffmpeg.org/wiki/Scaling
// If we'd like to keep the aspect ratio, we need to specify only one component, either width or height.
// Some codecs require the size of width and height to be a multiple of n. You can achieve this by setting the width or height to -n.
2020-11-13 00:14:59 +01:00
// SetVideoScalingWidth will set the scaled video width of this variant.
2020-06-26 02:44:47 +02:00
func ( v * HLSVariant ) SetVideoScalingWidth ( width int ) {
v . videoSize . Width = width
}
2020-11-13 00:14:59 +01:00
// SetVideoScalingHeight will set the scaled video height of this variant.
2020-06-26 02:44:47 +02:00
func ( v * HLSVariant ) SetVideoScalingHeight ( height int ) {
v . videoSize . Height = height
}
func ( v * HLSVariant ) getScalingString ( ) string {
2021-04-15 22:55:51 +02:00
return fmt . Sprintf ( "scale=%s" , v . videoSize . getString ( ) )
2020-06-26 02:44:47 +02:00
}
// Video Quality
2020-11-13 00:14:59 +01:00
// SetVideoBitrate will set the output bitrate of this variant's video.
2020-08-06 21:19:35 +02:00
func ( v * HLSVariant ) SetVideoBitrate ( bitrate int ) {
2020-06-26 02:44:47 +02:00
v . videoBitrate = bitrate
}
2020-08-06 21:19:35 +02:00
func ( v * HLSVariant ) getVideoQualityString ( t * Transcoder ) string {
2020-06-26 02:44:47 +02:00
if v . isVideoPassthrough {
return fmt . Sprintf ( "-map v:0 -c:v:%d copy" , v . index )
}
2021-05-06 03:21:27 +02:00
gop := v . framerate * t . currentLatencyLevel . SecondsPerSegment // force an i-frame every segment
2020-08-06 21:19:35 +02:00
// For limiting the output bitrate
// https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
// https://developer.apple.com/documentation/http_live_streaming/about_apple_s_http_live_streaming_tools
// 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.
cmd := [ ] string {
"-map v:0" ,
2021-04-15 22:55:51 +02:00
fmt . Sprintf ( "-c:v:%d %s" , v . index , t . codec . Name ( ) ) , // Video codec used for this variant
2020-11-20 07:07:28 +01:00
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
2021-04-15 22:55:51 +02:00
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
2020-11-20 07:07:28 +01:00
fmt . Sprintf ( "-r:v:%d %d" , v . index , v . framerate ) ,
2021-04-15 22:55:51 +02:00
t . codec . VariantFlags ( v ) ,
2020-08-06 21:19:35 +02:00
}
return strings . Join ( cmd , " " )
2020-06-26 02:44:47 +02:00
}
2020-11-13 00:14:59 +01:00
// SetVideoFramerate will set the output framerate of this variant's video.
2020-06-26 02:44:47 +02:00
func ( v * HLSVariant ) SetVideoFramerate ( framerate int ) {
v . framerate = framerate
}
2021-04-15 22:55:51 +02:00
// SetCPUUsageLevel will set the hardware usage of this variant.
func ( v * HLSVariant ) SetCPUUsageLevel ( level int ) {
v . cpuUsageLevel = level
2020-06-26 02:44:47 +02:00
}
// Audio Quality
2020-11-13 00:14:59 +01:00
// SetAudioBitrate will set the output framerate of this variant's audio.
2020-06-26 02:44:47 +02:00
func ( v * HLSVariant ) SetAudioBitrate ( bitrate string ) {
v . audioBitrate = bitrate
}
func ( v * HLSVariant ) getAudioQualityString ( ) string {
if v . isAudioPassthrough {
2020-12-02 09:19:55 +01:00
return fmt . Sprintf ( "-map a:0? -c:a:%d copy" , v . index )
2020-06-26 02:44:47 +02:00
}
2020-07-12 01:34:50 +02:00
// libfdk_aac is not a part of every ffmpeg install, so use "aac" instead
encoderCodec := "aac"
2020-12-02 09:19:55 +01:00
return fmt . Sprintf ( "-map a:0? -c:a:%d %s -b:a:%d %s" , v . index , encoderCodec , v . index , v . audioBitrate )
2020-06-26 02:44:47 +02:00
}
2020-11-13 00:14:59 +01:00
// AddVariant adds a new HLS variant to include in the output.
2020-06-26 02:44:47 +02:00
func ( t * Transcoder ) AddVariant ( variant HLSVariant ) {
2020-10-20 06:34:42 +02:00
variant . index = len ( t . variants )
2020-06-26 02:44:47 +02:00
t . variants = append ( t . variants , variant )
}
2020-11-13 00:14:59 +01:00
// SetInput sets the input stream on the filesystem.
2020-06-26 02:44:47 +02:00
func ( t * Transcoder ) SetInput ( input string ) {
t . input = input
}
2021-07-03 21:28:25 +02:00
// SetStdin sets the Stdin of the ffmpeg command.
func ( t * Transcoder ) SetStdin ( rtmp * io . PipeReader ) {
t . stdin = rtmp
}
2020-11-13 00:14:59 +01:00
// SetOutputPath sets the root directory that should include playlists and video segments.
2020-06-26 02:44:47 +02:00
func ( t * Transcoder ) SetOutputPath ( output string ) {
t . segmentOutputPath = output
}
2020-11-13 00:14:59 +01:00
// SetAppendToStream enables appending to the HLS stream instead of overwriting.
2020-06-26 02:44:47 +02:00
func ( t * Transcoder ) SetAppendToStream ( append bool ) {
t . appendToStream = append
}
2020-10-02 08:34:29 +02:00
2020-11-13 00:14:59 +01:00
// SetIdentifer enables appending a unique identifier to segment file name.
2020-10-02 08:34:29 +02:00
func ( t * Transcoder ) SetIdentifier ( output string ) {
t . segmentIdentifier = output
}
2020-10-14 23:07:38 +02:00
2021-02-19 08:05:52 +01:00
func ( t * Transcoder ) SetInternalHTTPPort ( port string ) {
2020-10-14 23:07:38 +02:00
t . internalListenerPort = port
}
2021-04-15 22:55:51 +02:00
func ( t * Transcoder ) SetCodec ( codecName string ) {
t . codec = getCodec ( codecName )
}