class TivoHMO::Adapters::StreamIO::Transcoder

Transcodes video to tivo format using the streamio gem (ffmpeg)

Public Instance Methods

transcode(writeable_io, format="video/x-tivo-mpeg") click to toggle source

TODO: add ability to pass through data (copy codec) for files that are already (partially?) in the right format for tivo. Check against a mapping of tivo serial->allowed_formats code.google.com/p/streambaby/wiki/video_compatibility

# File lib/tivohmo/adapters/streamio/transcoder.rb, line 17
def transcode(writeable_io, format="video/x-tivo-mpeg")
  tmpfile = Tempfile.new('tivohmo_transcode')
  begin
    transcode_thread = run_transcode(tmpfile.path, format)

    # give the transcode thread a chance to start up before we
    # start copying from it.  Not strictly necessary, but makes
    # the log messages show up in the right order
    sleep 0.1

    run_copy(tmpfile.path, writeable_io, transcode_thread)
  ensure
    tmpfile.close
    tmpfile.unlink
  end

  nil
end
transcoder_options(format="video/x-tivo-mpeg") click to toggle source
# File lib/tivohmo/adapters/streamio/transcoder.rb, line 36
def transcoder_options(format="video/x-tivo-mpeg")
  opts = {
      video_max_bitrate: 30_000_000,
      buffer_size: 4096,
      audio_bitrate: 448_000,
      format: format,
      custom: []
  }

  opts = select_video_frame_rate(opts)
  opts = select_video_dimensions(opts)
  opts = select_video_codec(opts)
  opts = select_video_bitrate(opts)
  opts = select_audio_codec(opts)
  opts = select_audio_bitrate(opts)
  opts = select_audio_sample_rate(opts)
  opts = select_container(opts)
  opts = select_subtitle(opts)

  custom = opts.delete(:custom)
  opts[:custom] = custom.join(" ") if custom
  opts.delete(:format)

  opts
end

Protected Instance Methods

movie() click to toggle source
# File lib/tivohmo/adapters/streamio/transcoder.rb, line 64
def movie
  @movie ||= FFMPEG::Movie.new(item.file)
end
run_copy(transcoded_filename, writeable_io, transcode_thread) click to toggle source

we could avoid this if streamio-ffmpeg had a way to output to an IO, but it only supports file based output for now, so have to manually copy the file's bytes to our output stream

# File lib/tivohmo/adapters/streamio/transcoder.rb, line 266
def run_copy(transcoded_filename, writeable_io, transcode_thread)
  logger.info "Starting stream copy from: #{transcoded_filename}"
  file = File.open(transcoded_filename, 'rb')
  begin
    bytes_copied = 0

    # copying the IO from transcoded file to web output
    # stream is faster than the transcoding, and thus we
    # hit eof before transcode is done.  Therefore we need
    # to keep retrying while the transcode thread is alive,
    # then to avoid a race condition at the end, we keep
    # going till we've copied all the bytes
    while transcode_thread.alive? || bytes_copied < File.size(transcoded_filename)
      # sleep a bit at start of thread so we don't have a
      # wasteful tight loop when transcoding is really slow
      sleep 0.2

      while data = file.read(4096)
        break unless data.size > 0
        writeable_io << data
        bytes_copied += data.size
        end
      end

    logger.info "Stream copy completed, #{bytes_copied} bytes copied"
  rescue => e
    logger.error ("Stream copy failed: #{e}")
    transcode_thread[:halt] = true
  ensure
    file.close
  end
end
run_transcode(output_filename, format) click to toggle source
# File lib/tivohmo/adapters/streamio/transcoder.rb, line 231
def run_transcode(output_filename, format)

  logger.info "Movie Info: " +
                  video_info.collect {|k, v| "#{k}=#{v.inspect}"}.join(' ')

  opts = transcoder_options(format)

  logger.info "Transcoding options: " +
                   opts.collect {|k, v| "#{k}='#{v}'"}.join(' ')


  aspect_opt = opts.delete(:preserve_aspect_ratio)
  t_opts = {}
  t_opts[:preserve_aspect_ratio] = aspect_opt if aspect_opt


  transcode_thread = Thread.new do
    begin
      logger.info "Starting transcode of '#{movie.path}' to '#{output_filename}'"
      transcoded_movie = movie.transcode(output_filename, opts, t_opts) do |progress|
        logger.debug ("[%3i%%] Transcoding #{File.basename(movie.path)}" % (progress * 100).to_i)
        raise "Halted" if Thread.current[:halt]
      end
      logger.info "Transcoding completed, transcoded file size: #{File.size(output_filename)}"
    rescue => e
      logger.error ("Transcode failed: #{e}")
    end
  end

  return transcode_thread
end
select_audio_bitrate(opts) click to toggle source
# File lib/tivohmo/adapters/streamio/transcoder.rb, line 101
def select_audio_bitrate(opts)
  # transcode assumes unit of Kbit, whilst video_info has unit of bit
  opts[:audio_bitrate] = (opts[:audio_bitrate] / 1000).to_i

  opts
end
select_audio_codec(opts) click to toggle source
# File lib/tivohmo/adapters/streamio/transcoder.rb, line 108
def select_audio_codec(opts)
  if video_info[:audio_codec]
    if AUDIO_CODECS.any? { |ac| video_info[:audio_codec] =~ /#{ac}/ }
      opts[:audio_codec] = 'copy'
      if video_info[:video_codec] =~ /mpeg2video/
        opts[:custom] << "-copyts"
      end
    else
      opts[:audio_codec] = 'ac3'
    end
  end
  opts
end
select_audio_sample_rate(opts) click to toggle source
# File lib/tivohmo/adapters/streamio/transcoder.rb, line 90
def select_audio_sample_rate(opts)
  if video_info[:audio_sample_rate]
    if AUDIO_SAMPLE_RATES.include?(video_info[:audio_sample_rate])
      opts[:audio_sample_rate] = video_info[:audio_sample_rate]
    else
      opts[:audio_sample_rate] = 48000
    end
  end
  opts
end
select_container(opts) click to toggle source
# File lib/tivohmo/adapters/streamio/transcoder.rb, line 81
def select_container(opts)
  if opts[:format] == 'video/x-tivo-mpeg-ts'
    opts[:custom] << "-f mpegts"
  else
    opts[:custom] << "-f vob"
  end
  opts
end
select_subtitle(opts) click to toggle source
# File lib/tivohmo/adapters/streamio/transcoder.rb, line 203
def select_subtitle(opts)

  st = item.subtitle
  if st
    case st.type
      when :file
        code = st.language_code
        file = st.location

        if File.exist?(file)
          logger.info "Using subtitles present at: #{file}"
          opts[:custom] << "-vf subtitles=\"#{file}\""
        else
          logger.error "Subtitle doesn't exist at: #{file}"
        end
      when :embedded
        file = item.file
        idx = st.location
        logger.info "Using embedded subtitles [#{idx}] present at: #{file}"
        opts[:custom] << "-vf subtitles=\"#{file}\":si=#{idx}"
      else
        logger.error "Unknown subtitle type: #{st.type}"
    end
  end

  opts
end
select_video_bitrate(opts) click to toggle source
# File lib/tivohmo/adapters/streamio/transcoder.rb, line 122
def select_video_bitrate(opts)
  vbr = video_info[:video_bitrate]
  default_vbr = 16_384_000

  if vbr && vbr > 0
    if vbr >= opts[:video_max_bitrate]
      opts[:video_bitrate] = (opts[:video_max_bitrate] * 0.95).to_i
    elsif vbr > default_vbr
      opts[:video_bitrate] = vbr
    else
      opts[:video_bitrate] = default_vbr
    end
  end

  opts[:video_bitrate] ||= default_vbr

  # transcode assumes unit of Kbit, whilst video_info has unit of bit
  opts[:video_bitrate] = (opts[:video_bitrate] / 1000).to_i
  opts[:video_max_bitrate] = (opts[:video_max_bitrate] / 1000).to_i

  opts
end
select_video_codec(opts) click to toggle source
# File lib/tivohmo/adapters/streamio/transcoder.rb, line 145
def select_video_codec(opts)
  if VIDEO_CODECS.any? { |vc| video_info[:video_codec] =~ /#{vc}/ }
    opts[:video_codec] = 'copy'
    if video_info[:video_codec] =~ /h264/
      opts[:custom] << "-bsf h264_mp4toannexb"
    end
  else
    opts[:video_codec] = 'mpeg2video'
    opts[:custom] << "-pix_fmt yuv420p"
  end
  opts
end
select_video_dimensions(opts) click to toggle source
# File lib/tivohmo/adapters/streamio/transcoder.rb, line 158
def select_video_dimensions(opts)
  video_width = video_info[:width].to_i
  VIDEO_WIDTHS.each do |w|
    w = w.to_i
    if video_width >= w
      video_width = w
      opts[:preserve_aspect_ratio] = :width
      break
    end
  end
  video_width = VIDEO_WIDTHS.last.to_i unless video_width

  video_height = video_info[:height].to_i
  VIDEO_WIDTHS.each do |h|
    h = h.to_i
    if video_height >= h
      video_height = h
      opts[:preserve_aspect_ratio] = :height
      break
    end
  end
  video_height = VIDEO_HEIGHTS.last.to_i unless video_height

  opts[:resolution] = "#{video_width}x#{video_height}"
  opts[:preserve_aspect_ratio] ||= :height
  opts
end
select_video_frame_rate(opts) click to toggle source
# File lib/tivohmo/adapters/streamio/transcoder.rb, line 186
def select_video_frame_rate(opts)

  frame_rate = video_info[:frame_rate]
  if frame_rate =~ /\A[0-9\.]+\Z/
    frame_rate = frame_rate.to_f
  elsif frame_rate =~ /\A\((\d+)\/(\d+)\)\Z/
    frame_rate = $1.to_f / $2.to_f
  end

  VIDEO_FRAME_RATES.each do |r|
    opts[:frame_rate] = r
    break if frame_rate >= r.to_f
  end

  opts
end
video_info() click to toggle source
# File lib/tivohmo/adapters/streamio/transcoder.rb, line 68
def video_info
  @video_info ||= begin
    info_attrs = %w[
      path duration time bitrate rotation creation_time
      video_stream video_codec video_bitrate colorspace dar
      audio_stream audio_codec audio_bitrate audio_sample_rate
      calculated_aspect_ratio size audio_channels frame_rate container
      resolution width height
    ]
    Hash[info_attrs.collect {|attr| [attr.to_sym, movie.send(attr)] }]
  end
end