Skip to content

Instantly share code, notes, and snippets.

@biomancer
Created September 18, 2014 14:48
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save biomancer/8d139177f520b9dd3495 to your computer and use it in GitHub Desktop.
Save biomancer/8d139177f520b9dd3495 to your computer and use it in GitHub Desktop.
Generating i-frame and byterange m3u8 playlist from .ts video file for usage in HLS
module PlaylistBuilder
SEG_DURATION, PKT_SIZE, PKT_POS, PKT_TIME = 0,1,2,3
def self.extract_iframes_data(video_filepath)
raise "#{video_filepath} does not exists!" unless File.exists?(video_filepath)
iframes_data = []
cmd = "ffprobe -show_frames -select_streams v -of compact -show_entries packet=pts_time,codec_type,pos:frame=pict_type,pkt_pts_time,pkt_size,pkt_pos -i #{video_filepath.shellescape}"
frames_and_packets = nil
r = Benchmark.measure('') do
frames_and_packets = `#{cmd}`.split("\n")
end
puts r
iframes = frames_and_packets.grep(/pict_type=I/)
next_pkt_pts_time = nil
iframes.each_with_index do |iframe, index|
pkt_pts_time = next_pkt_pts_time || iframe.match(/.*pkt_pts_time=([0-9]*.[0-9]*).*/)[1]
if index >= iframes.count - 1
next_pkt_pts_time = frames_and_packets.last.match(/.*pkt_pts_time=([0-9]*.[0-9]*).*/)[1]
else
next_pkt_pts_time = iframes[index + 1].match(/.*pkt_pts_time=([0-9]*.[0-9]*).*/)[1]
end
pkt_size = iframe.match(/.*pkt_size=([0-9]*).*/)[1]
pkt_pos = iframe.match(/.*pkt_pos=([0-9]*).*/)[1]
segment_duration = (next_pkt_pts_time.to_f - pkt_pts_time.to_f).round(5)
iframes_data << [segment_duration, pkt_size.to_i, pkt_pos.to_i, pkt_pts_time.to_f.round(5)]
end
packets_pos = frames_and_packets.map{|p| p.match(/^packet.*pos=([0-9]*).*/).try{|m| m[1].to_i}}.compact
packets_pos_reversed = {}
packets_pos.each_with_index do |pos, i|
packets_pos_reversed[pos] = i
end
iframes_data.each do |iframe_data|
iframe_size = nil
if packets_pos_reversed[iframe_data[PKT_POS]] && packets_pos[packets_pos_reversed[iframe_data[PKT_POS]] + 1]
# https://github.com/pbs/iframe-playlist-generator/blob/master/iframeplaylistgenerator/generator.py
# "We compared the output of our library to Apple's example streams, and were off by 188 bytes
# for each I-frame byte-range."
# biomancer: Even with this fix result is sometimes off by extra 188 (188*2 total), for random chunks,
# I have not found any correlation with other packet parameters
iframe_size = packets_pos[packets_pos_reversed[iframe_data[PKT_POS]] + 1] - iframe_data[PKT_POS] + 188
end
iframe_data[PKT_SIZE] = iframe_size if iframe_size
end
iframes_data
end
def self.build_byterange_playlist(iframes_data, filesize, options = {})
options[:target_duration] ||= 10
playlist = <<-EOS_TOKEN
#EXTM3U
#EXT-X-TARGETDURATION:#{options[:target_duration]}
#EXT-X-VERSION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
EOS_TOKEN
last_iframe = [0,0,0,0]
iframes_data.each_with_index do |iframe_data, index|
duration_from_last_iframe = iframe_data[PKT_TIME] - last_iframe[PKT_TIME]
# playlist += "\n#DEBUG: duration_from_last_iframe: #{duration_from_last_iframe}\n"
if !iframes_data[index+1]
#last part
if duration_from_last_iframe + iframe_data[SEG_DURATION] > options[:target_duration]
playlist += byterange_chunk(
duration_from_last_iframe.round(5),
iframe_data[PKT_POS] - last_iframe[PKT_POS],
last_iframe[PKT_POS],
options[:url_filename]
)
last_iframe = iframe_data
duration_from_last_iframe = 0
end
playlist += byterange_chunk(
(duration_from_last_iframe + iframe_data[SEG_DURATION]).round(5),
filesize - last_iframe[PKT_POS],
last_iframe[PKT_POS],
options[:url_filename]
)
elsif iframes_data[index+1][PKT_TIME] - last_iframe[PKT_TIME] >= options[:target_duration]
#first or mid part
playlist += byterange_chunk(
duration_from_last_iframe.round(5),
iframe_data[PKT_POS] - last_iframe[PKT_POS],
last_iframe[PKT_POS],
options[:url_filename]
)
last_iframe = iframe_data
end
end
playlist += "#EXT-X-ENDLIST\n"
end
def self.build_iframe_playlist(iframes_data, options = {})
options[:target_duration] ||= iframes_data.map(&:first).max.ceil
playlist = <<-EOS_TOKEN
#EXTM3U
#EXT-X-TARGETDURATION:#{options[:target_duration]}
#EXT-X-VERSION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-I-FRAMES-ONLY
EOS_TOKEN
iframes_data.each do |iframe_data|
playlist += byterange_chunk(
iframe_data[SEG_DURATION],
iframe_data[PKT_SIZE],
iframe_data[PKT_POS],
options[:url_filename]
)
end
playlist += "#EXT-X-ENDLIST\n"
end
def self.byterange_chunk(duration, size, pos, file)
<<-EOS_TOKEN
#EXTINF:#{duration},
#EXT-X-BYTERANGE:#{size}@#{pos}
#{file}
EOS_TOKEN
end
def self.build_byterange_playlist_from_file(video_filepath, output_filepath, options = {})
iframes_data = extract_iframes_data(video_filepath)
options[:url_filename] ||= File.basename video_filepath
playlist = build_byterange_playlist(iframes_data, File.size(video_filepath), options)
File.open(output_filepath, 'w') {|f| f.write(playlist) }
end
def self.build_iframe_playlist_from_file(video_filepath, output_filepath, options = {})
iframes_data = extract_iframes_data(video_filepath)
options[:url_filename] ||= File.basename video_filepath
playlist = build_iframe_playlist(iframes_data, options)
File.open(output_filepath, 'w') {|f| f.write(playlist) }
end
end
@biomancer
Copy link
Author

Usage example:

PlaylistBuilder.build_byterange_playlist_from_file('/home/me/somevideo.ts', '/home/me/byterange_playlist.m3u8', {target_duration: 7})

PlaylistBuilder.build_iframe_playlist_from_file('/home/me/somevideo.ts', '/home/me/iframe_playlist.m3u8', {target_duration: 7})

@eightsixeight
Copy link

how about getting validity of an online file ? uri needed.. is that possible ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment