Difference between revisions of "Example Script using mythtranscode in fifodir mode"
From MythTV Official Wiki
Line 1: | Line 1: | ||
− | The script pipes the output of mythtranscode into ffmpeg, using libx264 to compress the video. The intended use is to set it up as a myth user job | + | The script pipes the output of mythtranscode into ffmpeg, using libx264 to compress the video. The intended use is to set it up as a myth user job. |
<pre> | <pre> | ||
− | #!/bin/ | + | #!/usr/bin/ruby -w |
− | + | require 'time' | |
+ | require 'fileutils' | ||
+ | require 'tmpdir' | ||
− | + | $stdout.reopen("/var/log/mythtv/mythar#{Time.now.strftime '%F-%T'}.log", "w") | |
− | + | $stderr.reopen($stdout) | |
− | + | ||
− | + | def usage | |
− | + | print <<-END | |
− | + | Usage: #{$0} --chanid <channel id> --starttime <start time> --title <title> [--subtitle <sub title>] --outdir <directory> [options] | |
− | + | #{$0} --chanid <channel id> --starttime <start time> -outfile <path> [options] | |
− | + | ||
− | + | --chanid <channel id> The channel id for the recording. REQUIRED. | |
− | + | --starttime <start time> The start time for the recording. REQUIRED. | |
− | + | --title <title> The title of the recording. | |
− | + | --subtitle <subtitle> The subtitle of the recording. | |
− | + | --outdir <path> The path of the directory for the output file. If one of | |
− | + | --title --subtitle and --outdir are specified, then both --title and --outdir must | |
− | + | be, and an output file will be chosen on their basis, avoiding | |
− | + | conflict with existing files. | |
− | + | --outfile <path> The exact path of the output file. ALTERNATIVE to | |
− | + | --title --subtitle and --outdir. | |
− | + | --vsize <width>x<height> Scale video to specified resolution (the aspect ratio will | |
− | + | be maintained using non-square pixels). If not specified, | |
− | + | the source resolution will be maintained. | |
− | + | --interlaced Assume the source is interlaced and maintain the interlace, | |
− | + | provided --vsize not specified. If --vsize is specified then | |
− | + | recode at double the frame rate. | |
+ | --passthrough Pass through the audio unaltered if possible. | ||
+ | --quality <quality> Quality for compression (default 20.0) | ||
+ | END | ||
+ | end | ||
# Read arguments | # Read arguments | ||
− | + | chanid = nil | |
− | + | starttime = nil | |
− | + | title = nil | |
− | + | subtitle = nil | |
− | + | outdir = nil | |
− | + | outfile = nil | |
− | + | vsize = nil | |
− | + | interlaced = false | |
− | + | passthrough = false | |
− | + | quality = '20.0' | |
+ | |||
+ | processOption = Proc.new do |args| | ||
+ | raise "Misspecified option: #{args.join(', ')}" unless args.length == 2 | ||
+ | option = args[0].sub(/^--/, '') | ||
+ | val = args[1] | ||
+ | eval("#{option} = val") | ||
+ | end | ||
+ | |||
+ | processFlag = Proc.new do |args| | ||
+ | raise "Misspecified option: #{args.join(', ')}" unless args.length == 1 | ||
+ | option = args[0].sub(/^--/, '') | ||
+ | eval("#{option} = true") | ||
+ | end | ||
+ | |||
+ | handler = {'--chanid' => processOption, | ||
+ | '--starttime' => processOption, | ||
+ | '--title' => processOption, | ||
+ | '--subtitle' => processOption, | ||
+ | '--outdir' => processOption, | ||
+ | '--outfile' => processOption, | ||
+ | '--vsize' => processOption, | ||
+ | '--interlaced' => processFlag, | ||
+ | '--passthrough' => processFlag, | ||
+ | '--quality' => processOption} | ||
+ | |||
+ | |||
+ | ARGV.slice_before{|arg| arg =~ /^--/}.each do |par| | ||
+ | raise "Unrecognised option: #{par.join(', ')}" unless handler.has_key? par[0] | ||
+ | handler[par[0]].call(par) | ||
+ | end | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | if chanid.nil? || starttime.nil? | |
− | if | + | raise '--chanid and --starttime required' |
− | + | end | |
− | |||
− | |||
− | |||
− | |||
− | if | + | if outfile.nil? && outdir.nil? |
− | + | raise 'Use either --outfile, or both --title --outdir, optionally with --subtitle' | |
− | + | end | |
− | |||
− | |||
− | |||
− | if | + | if outfile && (title || subtitle || outdir) |
− | + | raise 'Use either --outfile, or both --title --outdir, optionally with --subtitle' | |
− | + | end | |
− | |||
− | |||
− | |||
− | if | + | if (title || outdir || subtitle) && ! (title && outdir) |
− | + | raise 'Both --title and --outdir required for this mode of oparation' | |
− | + | end | |
− | |||
− | |||
− | |||
− | + | mythPassthrough = passthrough ? '--passthrough' : '' | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
#Detect source formats | #Detect source formats | ||
− | + | info=`mythtranscode -v general --chanid #{chanid} --starttime #{starttime} --fifoinfo #{mythPassthrough}` | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | svWidth = nil | |
− | + | svHeight = nil | |
− | + | svAspect = nil | |
− | + | svRate = nil | |
− | + | saFmt = nil | |
− | + | saChans = nil | |
− | + | saRate = nil | |
− | + | info.each_line do |line| | |
− | + | case line.chomp | |
− | case | + | when / FifoVideoWidth (.*)$/ then svWidth = $1 |
− | + | when / FifoVideoHeight (.*)$/ then svHeight = $1 | |
− | + | when / FifoVideoAspectRatio (.*)$/ then svAspect = $1 | |
− | + | when / FifoVideoFrameRate (.*)$/ then svRate = $1 | |
− | + | when / FifoAudioFormat (.*)$/ then saFmt = $1 | |
− | + | when / FifoAudioChannels (.*)$/ then saChans = $1 | |
− | + | when / FifoAudioHz (.*)$/ then saRate = $1 | |
− | + | when / FifoAudioSampleRate (.*)$/ then saRate = $1 | |
− | + | end | |
− | + | end | |
− | |||
− | if | + | if !svWidth || !svHeight || !svAspect || !svRate || !saFmt || !saChans || !saRate |
− | + | print info | |
− | + | raise 'Failed to derive fifo formats' | |
− | + | end | |
− | |||
− | + | svSize="#{svWidth}x#{svHeight}" | |
− | if | + | svAspect = '16:9' if svAspect == '1.77778' |
− | + | svAspect = '4:3' if svAspect == '1.33333' | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | if | + | saFmt = 'latm' if saFmt == 'acc_latm' |
− | |||
− | |||
− | |||
− | + | if saFmt == 'latm' | |
− | + | ext = '.mkv' | |
− | + | fFmt = 'matroska' | |
− | + | else | |
− | + | ext = '.mp4' | |
− | + | fFmt = 'mp4' | |
− | + | end | |
− | |||
# If --outfile not specified, use --outdir --title and --subtitle | # If --outfile not specified, use --outdir --title and --subtitle | ||
− | if | + | if !outfile |
− | + | obName = File.join(outdir, title) | |
− | + | obName += " - #{subtitle}" if (subtitle && subtitle.length > 0) | |
− | |||
− | + | (0..Float::INFINITY).each do |i| | |
− | + | outfile = obName + (i == 0 ? "" : "(#{i})") + ext | |
− | + | break if !File.exist? outfile | |
− | + | end | |
− | + | end | |
− | |||
− | |||
− | |||
# Can do pass through only with formats that ffmpeg will take as input | # Can do pass through only with formats that ffmpeg will take as input | ||
− | + | passthrough = false unless %w{ac3 dts mp3 latm}.include? saFmt | |
− | |||
− | |||
− | } | ||
# Tell ffmpeg what audio to expect and what to do with it | # Tell ffmpeg what audio to expect and what to do with it | ||
− | if | + | if passthrough |
− | + | mythPassthrough = '--passthrough' | |
− | + | audInDesc = "-f #{saFmt}" | |
− | + | audCodec = 'copy' | |
− | |||
− | |||
else | else | ||
− | + | mythPassthrough = '' | |
− | + | audInDesc = "-f s16le -ac 2 -ar #{saRate}" | |
− | + | audCodec = 'ac3 -ab 160k' | |
− | + | end | |
− | + | ||
+ | |||
+ | filters = [] | ||
+ | vFilter = nil | ||
+ | vRate = nil | ||
+ | vFlags = nil | ||
− | + | filters.push("crop=#{svWidth}:1080:0:0") if svHeight == '1088' | |
− | # | ||
− | if | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | if interlaced | |
− | + | if vsize | |
− | |||
− | if | ||
− | |||
− | if | ||
− | |||
# If scaling interlaced video, we must deinterlace first | # If scaling interlaced video, we must deinterlace first | ||
− | + | filters.push('yadif=3') | |
− | + | vRate = "-r #{2 * svRate.to_i}" | |
− | |||
else | else | ||
− | |||
# Otherwise maintain the interlace | # Otherwise maintain the interlace | ||
− | + | vFlags = '-flags +ilme+ildct' | |
− | + | end | |
− | + | end | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | } | + | if vsize |
+ | filters.push("scale=#{vsize.sub('x', ':')}") | ||
+ | end | ||
− | + | vFilter = '-vf ' + filters.join(',') if filters.length > 0 | |
− | |||
− | |||
− | |||
# Create fifo dir | # Create fifo dir | ||
− | + | fifoDir = Dir.mktmpdir('mythar') | |
− | + | fifoAudio = File.join(fifoDir, 'audout') | |
− | + | fifoVideo = File.join(fifoDir, 'vidout') | |
− | |||
− | |||
− | |||
− | |||
− | # | + | transcodeCmd = "/usr/bin/mythtranscode --chanid #{chanid} --starttime #{starttime} --honorcutlist #{mythPassthrough} --fifodir #{fifoDir} --cleancut" |
− | + | ffmpegCmd = "/usr/bin/ffmpeg #{audInDesc} -i #{fifoAudio} -f rawvideo -top 1 -pix_fmt yuv420p -s #{svSize} -r #{svRate} -i #{fifoVideo} -threads 2 #{vFilter} -vcodec libx264 -preset medium -profile high #{vFlags} -crf #{quality} #{vRate} -aspect #{svAspect} -acodec #{audCodec} -ar #{saRate} -f #{fFmt} \"#{outfile}\"" | |
− | |||
− | + | puts transcodeCmd | |
− | + | puts | |
+ | puts ffmpegCmd | ||
+ | puts | ||
− | + | pid = 0 | |
− | + | begin | |
+ | pid = Process.spawn(transcodeCmd) | ||
+ | while !File.exist?(fifoAudio) || !File.exist?(fifoVideo) | ||
+ | raise 'Transcode died before creating pipes' unless Process.wait(pid, Process::WNOHANG).nil? | ||
+ | sleep(1) | ||
+ | end | ||
+ | system(ffmpegCmd) | ||
+ | ensure | ||
+ | if pid != 0 | ||
+ | Process.kill('KILL', pid) | ||
+ | Process.wait(pid) | ||
+ | end | ||
+ | FileUtils.rm_rf fifoDir | ||
+ | end | ||
</pre> | </pre> |
Latest revision as of 22:14, 25 February 2015
The script pipes the output of mythtranscode into ffmpeg, using libx264 to compress the video. The intended use is to set it up as a myth user job.
#!/usr/bin/ruby -w require 'time' require 'fileutils' require 'tmpdir' $stdout.reopen("/var/log/mythtv/mythar#{Time.now.strftime '%F-%T'}.log", "w") $stderr.reopen($stdout) def usage print <<-END Usage: #{$0} --chanid <channel id> --starttime <start time> --title <title> [--subtitle <sub title>] --outdir <directory> [options] #{$0} --chanid <channel id> --starttime <start time> -outfile <path> [options] --chanid <channel id> The channel id for the recording. REQUIRED. --starttime <start time> The start time for the recording. REQUIRED. --title <title> The title of the recording. --subtitle <subtitle> The subtitle of the recording. --outdir <path> The path of the directory for the output file. If one of --title --subtitle and --outdir are specified, then both --title and --outdir must be, and an output file will be chosen on their basis, avoiding conflict with existing files. --outfile <path> The exact path of the output file. ALTERNATIVE to --title --subtitle and --outdir. --vsize <width>x<height> Scale video to specified resolution (the aspect ratio will be maintained using non-square pixels). If not specified, the source resolution will be maintained. --interlaced Assume the source is interlaced and maintain the interlace, provided --vsize not specified. If --vsize is specified then recode at double the frame rate. --passthrough Pass through the audio unaltered if possible. --quality <quality> Quality for compression (default 20.0) END end # Read arguments chanid = nil starttime = nil title = nil subtitle = nil outdir = nil outfile = nil vsize = nil interlaced = false passthrough = false quality = '20.0' processOption = Proc.new do |args| raise "Misspecified option: #{args.join(', ')}" unless args.length == 2 option = args[0].sub(/^--/, '') val = args[1] eval("#{option} = val") end processFlag = Proc.new do |args| raise "Misspecified option: #{args.join(', ')}" unless args.length == 1 option = args[0].sub(/^--/, '') eval("#{option} = true") end handler = {'--chanid' => processOption, '--starttime' => processOption, '--title' => processOption, '--subtitle' => processOption, '--outdir' => processOption, '--outfile' => processOption, '--vsize' => processOption, '--interlaced' => processFlag, '--passthrough' => processFlag, '--quality' => processOption} ARGV.slice_before{|arg| arg =~ /^--/}.each do |par| raise "Unrecognised option: #{par.join(', ')}" unless handler.has_key? par[0] handler[par[0]].call(par) end if chanid.nil? || starttime.nil? raise '--chanid and --starttime required' end if outfile.nil? && outdir.nil? raise 'Use either --outfile, or both --title --outdir, optionally with --subtitle' end if outfile && (title || subtitle || outdir) raise 'Use either --outfile, or both --title --outdir, optionally with --subtitle' end if (title || outdir || subtitle) && ! (title && outdir) raise 'Both --title and --outdir required for this mode of oparation' end mythPassthrough = passthrough ? '--passthrough' : '' #Detect source formats info=`mythtranscode -v general --chanid #{chanid} --starttime #{starttime} --fifoinfo #{mythPassthrough}` svWidth = nil svHeight = nil svAspect = nil svRate = nil saFmt = nil saChans = nil saRate = nil info.each_line do |line| case line.chomp when / FifoVideoWidth (.*)$/ then svWidth = $1 when / FifoVideoHeight (.*)$/ then svHeight = $1 when / FifoVideoAspectRatio (.*)$/ then svAspect = $1 when / FifoVideoFrameRate (.*)$/ then svRate = $1 when / FifoAudioFormat (.*)$/ then saFmt = $1 when / FifoAudioChannels (.*)$/ then saChans = $1 when / FifoAudioHz (.*)$/ then saRate = $1 when / FifoAudioSampleRate (.*)$/ then saRate = $1 end end if !svWidth || !svHeight || !svAspect || !svRate || !saFmt || !saChans || !saRate print info raise 'Failed to derive fifo formats' end svSize="#{svWidth}x#{svHeight}" svAspect = '16:9' if svAspect == '1.77778' svAspect = '4:3' if svAspect == '1.33333' saFmt = 'latm' if saFmt == 'acc_latm' if saFmt == 'latm' ext = '.mkv' fFmt = 'matroska' else ext = '.mp4' fFmt = 'mp4' end # If --outfile not specified, use --outdir --title and --subtitle if !outfile obName = File.join(outdir, title) obName += " - #{subtitle}" if (subtitle && subtitle.length > 0) (0..Float::INFINITY).each do |i| outfile = obName + (i == 0 ? "" : "(#{i})") + ext break if !File.exist? outfile end end # Can do pass through only with formats that ffmpeg will take as input passthrough = false unless %w{ac3 dts mp3 latm}.include? saFmt # Tell ffmpeg what audio to expect and what to do with it if passthrough mythPassthrough = '--passthrough' audInDesc = "-f #{saFmt}" audCodec = 'copy' else mythPassthrough = '' audInDesc = "-f s16le -ac 2 -ar #{saRate}" audCodec = 'ac3 -ab 160k' end filters = [] vFilter = nil vRate = nil vFlags = nil filters.push("crop=#{svWidth}:1080:0:0") if svHeight == '1088' if interlaced if vsize # If scaling interlaced video, we must deinterlace first filters.push('yadif=3') vRate = "-r #{2 * svRate.to_i}" else # Otherwise maintain the interlace vFlags = '-flags +ilme+ildct' end end if vsize filters.push("scale=#{vsize.sub('x', ':')}") end vFilter = '-vf ' + filters.join(',') if filters.length > 0 # Create fifo dir fifoDir = Dir.mktmpdir('mythar') fifoAudio = File.join(fifoDir, 'audout') fifoVideo = File.join(fifoDir, 'vidout') transcodeCmd = "/usr/bin/mythtranscode --chanid #{chanid} --starttime #{starttime} --honorcutlist #{mythPassthrough} --fifodir #{fifoDir} --cleancut" ffmpegCmd = "/usr/bin/ffmpeg #{audInDesc} -i #{fifoAudio} -f rawvideo -top 1 -pix_fmt yuv420p -s #{svSize} -r #{svRate} -i #{fifoVideo} -threads 2 #{vFilter} -vcodec libx264 -preset medium -profile high #{vFlags} -crf #{quality} #{vRate} -aspect #{svAspect} -acodec #{audCodec} -ar #{saRate} -f #{fFmt} \"#{outfile}\"" puts transcodeCmd puts puts ffmpegCmd puts pid = 0 begin pid = Process.spawn(transcodeCmd) while !File.exist?(fifoAudio) || !File.exist?(fifoVideo) raise 'Transcode died before creating pipes' unless Process.wait(pid, Process::WNOHANG).nil? sleep(1) end system(ffmpegCmd) ensure if pid != 0 Process.kill('KILL', pid) Process.wait(pid) end FileUtils.rm_rf fifoDir end