class HLSDownload::HLS

Attributes

iframe_m3u8[RW]
logger[RW]
main_url[RW]
media_files[RW]
prefix[RW]
raw_url[RW]
sub_m3u8[RW]

Public Class Methods

new(url, logger = nil, prefix = nil) click to toggle source
# File lib/hls.rb, line 45
def initialize(url, logger = nil, prefix = nil)
  @raw_url = url
  @logger = logger || new_logger
  @main_url = URI.parse(url)
  @media_files = []
  @sub_m3u8 = []
  @prefix = prefix
  @iframe_m3u8 = []
  parse @main_url
end

Public Instance Methods

download!(opts = {}) click to toggle source
# File lib/hls.rb, line 56
def download!(opts = {})
  manifest_file = main_url.path.split('/').last
  base_url = main_url.to_s.gsub("/#{manifest_file}", '')
  @output_dir = opts[:output_dir] || 'out'
  FileUtils.mkdir_p @output_dir

  main_manifest_output = File.join(@output_dir, 'manifest.m3u8')
  main_manifest_contents = http_get(main_url)
  
  sub_m3u8.each do | m3u8 |
    output = nil
    if m3u8.prefix
      rel_path = File.join(m3u8.prefix, 'index.m3u8')
      output = File.join(@output_dir, rel_path)
      # rewrite contents
      main_manifest_contents.gsub!(m3u8.raw_url, rel_path)
    else
      output = m3u8.main_url.to_s.gsub(base_url, @output_dir)
    end
    manifest_contents = http_get(m3u8.main_url)
    m3u8.media_files.each_with_index do | media_file, i |
      media_output = nil
      if m3u8.prefix
        ext = File.extname(media_file.raw.split('?')[0].split('/').last)
        basename = "#{i+1}#{ext}"
        media_output = File.join(@output_dir, m3u8.prefix, basename)
        # rewrite contents
        manifest_contents.gsub!(media_file.raw, basename)
      else
        media_output = media_file.url.gsub(base_url, @output_dir)
      end
      download_file(URI.parse(media_file.url), media_output)
    end

    FileUtils.mkdir_p File.dirname(output)
    write(output, manifest_contents)
  end

  iframe_m3u8.each do | iframe |
    if iframe.raw.include?('://')
      unless iframe.raw.include?(base_url)
        raise HLSException.new('download I-FRAME-STREAM-INF with absolute url is not supported')
      end
    end

    iframe_output = iframe.url.to_s.gsub(base_url, @output_dir)
    
    iframe_manifest_contents = http_get(iframe.url)
    lines = iframe_manifest_contents.split("\n")
    files = lines.reject { |l| l.start_with? '#' }
    files = files.map { |l| l.gsub(' ', '') }
    files = files.reject { |l| l.empty? }
    files.each do |f|
      if f.include?('://')
        raise HLSException.new('download I-FRAME-STREAM-INF with absolute path media files is not supported')
      end
    end
    FileUtils.mkdir_p File.dirname(iframe_output)
    download_file(iframe.url, iframe_output)
  end
  
  write(main_manifest_output, main_manifest_contents)

  # TODO download recursively. i.e. allow downloading only 1 rendition
end

Private Instance Methods

download_file(url, output) click to toggle source
# File lib/hls.rb, line 213
def download_file(url, output)
  unless output == '/dev/null'
    logger.debug "downloading #{output}"
    dir = Pathname.new(output).dirname.to_s
    unless File.directory?(dir)
      logger.debug "creating dir: #{dir}"
      FileUtils.mkdir_p(dir) 
    end
  end
  resp = nil
  Net::HTTP.start(url.host) do |http|
    resp = http.get("#{url.path}?#{url.query}")
    if ['4', '5'].include? resp.code[0]
      raise HLSException.new("#{url} response status code: #{resp.code}")
    end
    write(output, resp.body)
  end
  resp.body
end
http_get(url) click to toggle source
# File lib/hls.rb, line 208
def http_get(url)
  logger.debug "http get #{url.to_s}"
  download_file(url, '/dev/null')
end
is_media_playlist?(man) click to toggle source
# File lib/hls.rb, line 233
def is_media_playlist?(man)
  MEDIA_FORMAT_EXTENSIONS.each do |ext|
    return true if man.include? ext
  end
  false
end
new_logger() click to toggle source
# File lib/hls.rb, line 240
def new_logger
  l = Logger.new STDOUT
  l.level = Logger::ERROR
  l
end
parse(url) click to toggle source
# File lib/hls.rb, line 124
def parse(url)
  man = http_get(url)
  logger.debug "parsing manifest"
  lines = man.split("\n")
  
  unless man.include? '.m3u8'
    # this is a variant playlist
    raise HLSException.new('unsupported media playlist') unless is_media_playlist? man
    logger.debug 'getting media url(s)'
    files = lines.reject { |l| l.start_with? '#' }
    files = files.map { |l| l.gsub(' ', '') }
    files = files.reject { |l| l.empty? }
    files.each_with_index do |f, i|
      raw = f
      unless f.include? '://'
        # relative path
        f_url = url.dup
        split_path = f_url.dup.path.split('/')
        split_path[-1] = f
        path = File.join(split_path)
        f_url.path = path.start_with?('/') ? path : "/#{path}"
        f = f_url.to_s
      end
      @media_files << HLSMediaFile.new(f, i, raw)
    end
    
    return
  end
  
  # this is a master playlist
  uris = []
  iframe_m3u8_uris = []
  lines.select { |l| l.include? '.m3u8' }.each do |l|
    is_iframe = false
    if l.start_with? '#'
      is_iframe = l.include?('I-FRAME-STREAM-INF')
      uri_res = nil
      l.split(',').each do | attribute |
        next unless attribute.start_with? 'URI'
        uri_res = attribute.gsub('URI=', '').gsub('"', '')
      end
      if is_iframe
        iframe_m3u8_uris << uri_res
      else
        uris << uri_res
      end
    else
      uris << l
    end
  end

  uris.each_with_index do |u, i|
    prefix = nil
    if u.include? '://'
      prefix = "presentation_#{i}"
    else
      # relative path
      s_url = url.dup
      split_path = s_url.dup.path.split('/')
      split_path[-1] = u
      path = File.join(split_path)
      s_url.path = path.start_with?('/') ? path : "/#{path}"
      u = s_url.to_s
    end
    logger.debug "sub playlist found #{u}"
    sub_m3u8 << HLS.new(u, logger, prefix)
  end

  iframe_m3u8_uris.each_with_index do |u, i|
    raw = u
    if u.include? '://'
    else
      # relative path
      s_url = url.dup
      split_path = s_url.dup.path.split('/')
      split_path[-1] = u
      path = File.join(split_path)
      s_url.path = path.start_with?('/') ? path : "/#{path}"
      u = s_url.to_s
    end
    @iframe_m3u8 << IFrameM3U8.new(u, i, raw)
  end
end
write(output, contents) click to toggle source
# File lib/hls.rb, line 246
def write(output, contents)
  open(output, "wb") do |file|
    file.write(contents)
  end
end