coderrr-video-accel 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +10 -0
- data/Rakefile +1 -0
- data/bin/vdl +3 -0
- data/bin/vplayer +7 -0
- data/lib/clip_nabber.rb +21 -0
- data/lib/core_ext.rb +17 -0
- data/lib/speed_stream.rb +137 -0
- data/lib/threadpool.rb +115 -0
- data/lib/vdl.rb +55 -0
- data/lib/video_player.rb +54 -0
- data/lib/youtube_speed_stream.rb +46 -0
- data/video-accel.gemspec +14 -0
- metadata +65 -0
data/README.markdown
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
Video what?
|
2
|
+
====
|
3
|
+
|
4
|
+
video-accel is an accelerator for streaming videos on the web. It acts very similar to a download accelerator.
|
5
|
+
|
6
|
+
A download accelerator will split a file up into a small (~5) number of even parts. It will then download each part simultaneously. This gets past the per-connection bandwidth cap and allows you to max out your total bandwidth cap. The problem with this is that it doesn't help you watch videos while they're downloading since the first 1/5 of the file will still be downloading at normal speed, and that's the part you want to start watching now.
|
7
|
+
|
8
|
+
So instead of splitting the file up into a small number of even parts, video-accel splits the file up into a large number of even parts and then downloads a small number at a time, but it downloads them *in order*. The fact that it downloads them in order is what allows you to still stream the video while it is being downloaded at a faster rate.
|
9
|
+
|
10
|
+
Currently video-accel only works with Linux.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
load 'video-accel.gemspec'
|
data/bin/vdl
ADDED
data/bin/vplayer
ADDED
data/lib/clip_nabber.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
class ClipNabber
|
4
|
+
URI = ::URI.parse("http://clipnabber.com/gethint.php?mode=1")
|
5
|
+
|
6
|
+
def initialize(url)
|
7
|
+
@url = url
|
8
|
+
end
|
9
|
+
|
10
|
+
def download_link
|
11
|
+
body = Net::HTTP.start(URI.host, URI.port) do |h|
|
12
|
+
h.request_get(URI.request_uri + "&url=#{@url}&sid=#{rand}", 'Referer' => 'http://clipnabber.com').body
|
13
|
+
end
|
14
|
+
|
15
|
+
if body =~ %r{<a href='([^']+).*?><strong>FLV download link}im
|
16
|
+
$1
|
17
|
+
else
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/core_ext.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
class IO
|
2
|
+
def while_reading(data = nil, &b)
|
3
|
+
while buf = readpartial_rescued(16384)
|
4
|
+
data << buf if data
|
5
|
+
yield buf if block_given?
|
6
|
+
end
|
7
|
+
data
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def readpartial_rescued(size)
|
13
|
+
readpartial(size)
|
14
|
+
rescue EOFError
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
end
|
data/lib/speed_stream.rb
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'cgi'
|
5
|
+
require 'threadpool'
|
6
|
+
require 'core_ext'
|
7
|
+
Thread.abort_on_exception = true
|
8
|
+
|
9
|
+
class SpeedStream
|
10
|
+
USER_AGENT = %{Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3}
|
11
|
+
BUF_SIZE = 1024
|
12
|
+
|
13
|
+
attr_reader :uri, :bytes_per_conn, :output_file, :file_handle, :concurrent_connections, :cookies
|
14
|
+
attr_accessor :on_progress
|
15
|
+
|
16
|
+
def initialize(uri, output_file)
|
17
|
+
@uri = URI.parse(uri)
|
18
|
+
@output_file = output_file
|
19
|
+
@write_mutex = Mutex.new
|
20
|
+
@finished = []
|
21
|
+
@bytes_per_conn = 30_000
|
22
|
+
@concurrent_connections = 10
|
23
|
+
@cookies = {}
|
24
|
+
@extra_file_length_headers = {'Range' => 'bytes=1-'}
|
25
|
+
end
|
26
|
+
|
27
|
+
def download!
|
28
|
+
@start_time = Time.now
|
29
|
+
@bytes_written = 0
|
30
|
+
pool = ThreadPool.new(concurrent_connections, 999999)
|
31
|
+
|
32
|
+
File.open(output_file, "wb") do |f|
|
33
|
+
@file_handle = f
|
34
|
+
|
35
|
+
begin
|
36
|
+
ranges.each do |range|
|
37
|
+
pool.add_work(range) do |range|
|
38
|
+
download_range range
|
39
|
+
end
|
40
|
+
end
|
41
|
+
ensure
|
42
|
+
pool.shutdown
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def download_range(range)
|
51
|
+
file_offset = range.begin
|
52
|
+
|
53
|
+
data = get_with_redirects(range)
|
54
|
+
write_file(file_offset, data)
|
55
|
+
|
56
|
+
process_progress(range)
|
57
|
+
end
|
58
|
+
|
59
|
+
def process_progress(range)
|
60
|
+
pos = range.begin/bytes_per_conn
|
61
|
+
@finished[pos] = 1
|
62
|
+
total_pos = file_length/bytes_per_conn
|
63
|
+
time_passed = Time.now - @start_time
|
64
|
+
|
65
|
+
completed_count = (@finished.index(nil)||@finished.size)
|
66
|
+
completed_bytes = completed_count * bytes_per_conn
|
67
|
+
bps = 1.0 * @bytes_written/time_passed
|
68
|
+
percent = 100.0 * completed_count / total_pos
|
69
|
+
|
70
|
+
on_progress.call(completed_bytes, bps, percent) if on_progress
|
71
|
+
end
|
72
|
+
|
73
|
+
def get_with_redirects(range)
|
74
|
+
loop do
|
75
|
+
Net::HTTP.start(uri.host, uri.port) do |h|
|
76
|
+
req = h.request_get(uri.request_uri, 'Range' => "bytes=#{range.begin}-#{range.end}", 'Cookie' => cookie_string)
|
77
|
+
if loc = req['location']
|
78
|
+
@uri = URI.parse(loc)
|
79
|
+
next
|
80
|
+
end
|
81
|
+
|
82
|
+
return req.body
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def write_file(pos, buf)
|
88
|
+
@bytes_written += buf.size
|
89
|
+
@write_mutex.synchronize do
|
90
|
+
file_handle.seek(pos, IO::SEEK_SET)
|
91
|
+
file_handle.write buf
|
92
|
+
file_handle.flush
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def ranges
|
97
|
+
@ranges ||= begin
|
98
|
+
rs = []
|
99
|
+
(file_length/bytes_per_conn).times do |n|
|
100
|
+
rs << (n*bytes_per_conn..(n+1)*bytes_per_conn)
|
101
|
+
end
|
102
|
+
rs[-1] = (rs[-1].begin..file_length)
|
103
|
+
rs
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def file_length
|
108
|
+
@file_length ||= begin
|
109
|
+
current_uri = uri
|
110
|
+
loop do
|
111
|
+
Net::HTTP.start(current_uri.host, current_uri.port) do |h|
|
112
|
+
headers = {'Host' => current_uri.host, 'User-Agent' => USER_AGENT, 'Cookie' => cookie_string}
|
113
|
+
headers.merge! @extra_file_length_headers
|
114
|
+
resp = h.request_head(current_uri.request_uri, headers)
|
115
|
+
|
116
|
+
cookies.merge! parse_cookies(resp['set-cookie']) if resp['set-cookie']
|
117
|
+
|
118
|
+
if loc = resp['location']
|
119
|
+
current_uri = URI.parse(loc)
|
120
|
+
next
|
121
|
+
end
|
122
|
+
|
123
|
+
@uri = current_uri
|
124
|
+
return resp['content-length'].to_i
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def parse_cookies(header)
|
131
|
+
Hash[*header.scan(/(?:^|, )(\w+)=([^;]+)/).flatten]
|
132
|
+
end
|
133
|
+
|
134
|
+
def cookie_string
|
135
|
+
cookies.map {|k,v| "#{k}=#{v}"}.join '; '
|
136
|
+
end
|
137
|
+
end
|
data/lib/threadpool.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'monitor'
|
3
|
+
|
4
|
+
class ThreadPool
|
5
|
+
|
6
|
+
class PoolStopped < Exception; end
|
7
|
+
|
8
|
+
def initialize(thread_size=10, queue_size=100)
|
9
|
+
@mutex = Monitor.new
|
10
|
+
@cv = @mutex.new_cond
|
11
|
+
@queue = []
|
12
|
+
@max_queue_size = queue_size
|
13
|
+
@threads = []
|
14
|
+
@stopped = false
|
15
|
+
thread_size.times { @threads << Thread.new { start_worker } }
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_work(*args, &callback)
|
19
|
+
push_task(Task.new(*args, &callback))
|
20
|
+
end
|
21
|
+
|
22
|
+
def push_task(task)
|
23
|
+
@mutex.synchronize do
|
24
|
+
raise PoolStopped.new if @stopped
|
25
|
+
@cv.wait_while { @max_queue_size > 0 && @queue.size >= @max_queue_size }
|
26
|
+
@queue.push(task)
|
27
|
+
@cv.broadcast
|
28
|
+
end
|
29
|
+
task
|
30
|
+
end
|
31
|
+
|
32
|
+
def pop_task
|
33
|
+
task = nil
|
34
|
+
@mutex.synchronize do
|
35
|
+
@cv.wait_while { @queue.size == 0 }
|
36
|
+
task = @queue.shift
|
37
|
+
@cv.broadcast
|
38
|
+
end
|
39
|
+
task
|
40
|
+
end
|
41
|
+
|
42
|
+
def shutdown
|
43
|
+
@mutex.synchronize do
|
44
|
+
@stopped = true
|
45
|
+
@threads.each { @queue.push(:stop) }
|
46
|
+
@cv.broadcast
|
47
|
+
end
|
48
|
+
@threads.each { |thread| thread.join }
|
49
|
+
end
|
50
|
+
|
51
|
+
def start_worker
|
52
|
+
while true
|
53
|
+
task = pop_task
|
54
|
+
return if task == :stop
|
55
|
+
task.execute
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# wait for current work to complete
|
60
|
+
def sync
|
61
|
+
tasks = @mutex.synchronize { @queue.dup }
|
62
|
+
tasks.each { |task| task.join }
|
63
|
+
end
|
64
|
+
|
65
|
+
class Task
|
66
|
+
|
67
|
+
attr_reader :result, :exception
|
68
|
+
|
69
|
+
def initialize(*args, &callback)
|
70
|
+
@args = args
|
71
|
+
@callback = callback
|
72
|
+
@done = false
|
73
|
+
@result = nil
|
74
|
+
@exception = nil
|
75
|
+
@mutex = Monitor.new
|
76
|
+
@cv = @mutex.new_cond
|
77
|
+
end
|
78
|
+
|
79
|
+
def execute
|
80
|
+
begin
|
81
|
+
@result = @callback.call(*@args)
|
82
|
+
rescue Exception => e
|
83
|
+
@exception = e
|
84
|
+
STDERR.puts "Error in thread #{Thread.current} - #{e}"
|
85
|
+
e.backtrace.each { |element| STDERR.puts(element) }
|
86
|
+
end
|
87
|
+
@mutex.synchronize do
|
88
|
+
@done = true
|
89
|
+
@cv.broadcast
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def join
|
94
|
+
@mutex.synchronize { @cv.wait_until { @done } }
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
#
|
101
|
+
#tasks = []
|
102
|
+
#tp = ThreadPool.new(10, 1000)
|
103
|
+
#sleep(1)
|
104
|
+
#100.times do |id|
|
105
|
+
# STDERR.puts "adding work"
|
106
|
+
# tasks << tp.add_work do
|
107
|
+
# puts "Running #{id} #{Thread.current}"
|
108
|
+
# sleep 5
|
109
|
+
# puts "Ending #{id} #{Thread.current}"
|
110
|
+
# end
|
111
|
+
#end
|
112
|
+
#
|
113
|
+
#puts "Waiting for shutdown"
|
114
|
+
#tp.shutdown
|
115
|
+
#puts "done"
|
data/lib/vdl.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH << File.dirname(__FILE__)
|
4
|
+
require 'optparse'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'clip_nabber'
|
7
|
+
require 'youtube_speed_stream'
|
8
|
+
|
9
|
+
skip_resolve = nil
|
10
|
+
player = "vplayer"
|
11
|
+
terminal = "gnome-terminal -e"
|
12
|
+
|
13
|
+
(opts = OptionParser.new do |o|
|
14
|
+
o.on("-s", "--skip-resolve") { skip_resolve = true }
|
15
|
+
o.on("-p", "--player CMD") { |a| player = a }
|
16
|
+
o.on("-t", "--terminal CMD") { |a| terminal = a }
|
17
|
+
end).parse! rescue (puts opts; exit)
|
18
|
+
url = ARGV.shift
|
19
|
+
url = "http://#{url}" if url !~ /https?:\/\//i
|
20
|
+
puts "URL: #{url}"
|
21
|
+
|
22
|
+
# random name
|
23
|
+
ltrs=('a'..'z').to_a
|
24
|
+
name = ([nil]*3).map{ltrs[rand(ltrs.size)]}*''
|
25
|
+
dir = File.expand_path('~/.movies')
|
26
|
+
FileUtils.mkdir_p dir
|
27
|
+
file = File.join dir, name
|
28
|
+
|
29
|
+
unless skip_resolve
|
30
|
+
puts "resolving url"
|
31
|
+
dl_url = ClipNabber.new(url).download_link
|
32
|
+
if ! (URI.parse(dl_url) rescue false)
|
33
|
+
puts "problem resolving #{url}"
|
34
|
+
exit
|
35
|
+
end
|
36
|
+
puts "resolved to #{dl_url}"
|
37
|
+
url = dl_url
|
38
|
+
end
|
39
|
+
|
40
|
+
klass = (url =~ /youtube\.com\//) ? YoutubeSpeedStream : SpeedStream
|
41
|
+
stream = klass.new url, file
|
42
|
+
|
43
|
+
started = false
|
44
|
+
stream.on_progress = lambda do |bytes, bps, percent|
|
45
|
+
print "\ec"
|
46
|
+
puts "downloading #{url} to #{file}..."
|
47
|
+
puts "#{'%.2f'%bps} b/s #{'%.2f%'%percent}"
|
48
|
+
|
49
|
+
if !started and bytes > 1_000_000
|
50
|
+
started = true
|
51
|
+
system(%{#{terminal} "#{player} #{file}" &})
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
stream.download!
|
data/lib/video_player.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
class VideoPlayer
|
2
|
+
MP_ARGS = [
|
3
|
+
"",
|
4
|
+
"-autosync 30 -mc 2.0",
|
5
|
+
"-autosync 0 -mc 0.0"
|
6
|
+
]
|
7
|
+
|
8
|
+
def initialize(file)
|
9
|
+
@file = file
|
10
|
+
@mplayer_arg = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def wait_for_cmd
|
14
|
+
rdy = IO.select([STDIN])
|
15
|
+
if rdy.first.include? STDIN
|
16
|
+
process_cmd STDIN.getc.chr
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def process_cmd(cmd)
|
21
|
+
puts "process #{cmd}"
|
22
|
+
case cmd
|
23
|
+
when 'q' then exit!
|
24
|
+
when 'm'
|
25
|
+
@pid.kill! if @pid
|
26
|
+
@pid = spawn("mplayer -msglevel all=0 #{MP_ARGS[@mplayer_arg]} #{@file}")
|
27
|
+
@mplayer_arg = (@mplayer_arg + 1)%MP_ARGS.size
|
28
|
+
when 'v'
|
29
|
+
@pid.kill! if @pid
|
30
|
+
@pid = spawn("vlc #{@file}")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def spawn(cmd)
|
35
|
+
pid = fork do
|
36
|
+
exec(cmd)
|
37
|
+
end
|
38
|
+
|
39
|
+
o = Object.new
|
40
|
+
(class <<o;self;end).send :define_method, :kill! do
|
41
|
+
puts "killing #{pid}"
|
42
|
+
ret = Process.kill 9, pid
|
43
|
+
end
|
44
|
+
|
45
|
+
o
|
46
|
+
end
|
47
|
+
|
48
|
+
def play
|
49
|
+
process_cmd('m')
|
50
|
+
loop do
|
51
|
+
wait_for_cmd
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'speed_stream'
|
2
|
+
|
3
|
+
class YoutubeSpeedStream < SpeedStream
|
4
|
+
def initialize(*a)
|
5
|
+
super
|
6
|
+
@concurrent_connections = 20
|
7
|
+
@extra_file_length_headers = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def get_with_redirects(range)
|
13
|
+
current_uri = uri
|
14
|
+
|
15
|
+
loop do
|
16
|
+
TCPSocket.open(current_uri.host, current_uri.port) do |s|
|
17
|
+
uri_str = current_uri.request_uri
|
18
|
+
if uri_str.include? "&start="
|
19
|
+
uri_str.gsub(/&start=\d+/, "&start=#{range.begin}")
|
20
|
+
else
|
21
|
+
uri_str << "&start=#{range.begin}"
|
22
|
+
end
|
23
|
+
s.write("GET #{uri_str} HTTP/1.1\r\nHost: #{current_uri.host}\r\nCookie: #{cookie_string}\r\n\r\n")
|
24
|
+
|
25
|
+
s.while_reading(data = "") { break if data =~ /\r?\n\r?\n/ }
|
26
|
+
|
27
|
+
if data =~ /^Location: (.+?)\r?$/
|
28
|
+
current_uri = URI.parse($1)
|
29
|
+
next
|
30
|
+
end
|
31
|
+
|
32
|
+
# strip off header and keep reading
|
33
|
+
data.gsub!(/\A.+?\r?\n\r?\n/m,'')
|
34
|
+
|
35
|
+
while buf = (s.readpartial(16384) rescue nil)
|
36
|
+
data << buf
|
37
|
+
break if range.begin + data.size > range.end
|
38
|
+
end
|
39
|
+
|
40
|
+
# strip FLV headers on all parts except first
|
41
|
+
start = range.begin == 0 ? 0 : 13
|
42
|
+
return data[start, range.end - range.begin]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/video-accel.gemspec
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
spec = Gem::Specification.new do |s|
|
2
|
+
s.name = "video-accel"
|
3
|
+
s.version = "0.0.1"
|
4
|
+
s.author = "coderrr"
|
5
|
+
s.email = "coderrr.contact@gmail.com"
|
6
|
+
# s.homepage = "http://blogs.cocoondev.org/crafterm/"
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.summary = "accelerate video streams"
|
9
|
+
s.files = ["lib/vdl.rb", "lib/speed_stream.rb", "lib/youtube_speed_stream.rb", "lib/threadpool.rb", "lib/core_ext.rb", "lib/clip_nabber.rb", "lib/video_player.rb", "bin/vdl", "bin/vplayer"] +
|
10
|
+
%w(Rakefile README.markdown video-accel.gemspec)
|
11
|
+
s.require_path = "lib"
|
12
|
+
s.bindir = "bin"
|
13
|
+
s.executables = ["vdl", "vplayer"]
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: coderrr-video-accel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- coderrr
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-10-09 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: coderrr.contact@gmail.com
|
18
|
+
executables:
|
19
|
+
- vdl
|
20
|
+
- vplayer
|
21
|
+
extensions: []
|
22
|
+
|
23
|
+
extra_rdoc_files: []
|
24
|
+
|
25
|
+
files:
|
26
|
+
- lib/vdl.rb
|
27
|
+
- lib/speed_stream.rb
|
28
|
+
- lib/youtube_speed_stream.rb
|
29
|
+
- lib/threadpool.rb
|
30
|
+
- lib/core_ext.rb
|
31
|
+
- lib/clip_nabber.rb
|
32
|
+
- lib/video_player.rb
|
33
|
+
- bin/vdl
|
34
|
+
- bin/vplayer
|
35
|
+
- Rakefile
|
36
|
+
- README.markdown
|
37
|
+
- video-accel.gemspec
|
38
|
+
has_rdoc: false
|
39
|
+
homepage:
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: "0"
|
50
|
+
version:
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: "0"
|
56
|
+
version:
|
57
|
+
requirements: []
|
58
|
+
|
59
|
+
rubyforge_project:
|
60
|
+
rubygems_version: 1.2.0
|
61
|
+
signing_key:
|
62
|
+
specification_version: 2
|
63
|
+
summary: accelerate video streams
|
64
|
+
test_files: []
|
65
|
+
|