=begin Copyright 2010, Roger Pack This file is part of Sensible Cinema. Sensible Cinema is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Sensible Cinema is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Sensible Cinema. If not, see . =end require 'win32/screenshot' require 'sane' require 'yaml' require File.dirname(__FILE__)+ '/ocr' require 'ffi' class ScreenTracker extend FFI::Library ffi_lib 'user32' # second parameter, pointer, LPRECT is FFI::MemoryPointer.new(:long, 4) # read it like rect.read_array_of_long(4) attach_function :GetWindowRect, [:long, :pointer], :int # returns a BOOL def self.new_from_yaml yaml, callback # callback can be nil, is used for timestamp changed stuff settings = YAML.load yaml return new(settings["name"], settings["x"], settings["y"], settings["width"], settings["height"], settings["use_class_name"], settings["digits"], callback) end attr_accessor :hwnd # digits are like {:hours => [100,5], :minute_tens, :minute_ones, :second_tens, :second_ones} # digits share the height start point, have their own x and width... def initialize name_or_regex, x, y, width, height, use_class_name=nil, digits=nil, callback=nil # cache to save us 0.00445136 per time LOL @name_or_regex = name_or_regex @use_class_name = use_class_name pps 'height', height, 'width', width if $VERBOSE raise 'poor dimentia' if width <= 0 || height <= 0 get_hwnd_loop_forever max_x, max_y = Win32::Screenshot::Util.dimensions_for(@hwnd) if(x < 0 || y < 0) if x < 0 x = max_x + x end if y < 0 y = max_y + y end end @x = x; @y = y; @x2 = x+width; @y2 = y+height; @callback = callback @max_x = max_x raise "poor width or wrong window #{@x2} #{max_x} #{x}" if @x2 > max_x || @x2 == x if @y2 > max_y || @y2 == y || @y2 <= 0 raise "poor height or wrong window selected #{@y2} > #{max_y} || #{@y2} == #{y} || #{@y2} <= 0" end if max_x == 0 || max_y == 0 # I don't think we can ever get here, because of the checks above raise 'window invisible?' end @digits = digits @previously_displayed_warning = false @dump_digit_count = 1 pps 'using x',@x, 'from x', x, 'y', @y, 'from y', y,'x2',@x2,'y2',@y2,'digits', @digits.inspect if $VERBOSE end def get_hwnd_loop_forever if @name_or_regex.to_s.downcase == 'desktop' # full screen option assert !@use_class_name # not an option @hwnd = hwnd = Win32::Screenshot::BitmapMaker.desktop_window return else raise if OS.mac? @hwnd = Win32::Screenshot::BitmapMaker.hwnd(@name_or_regex, @use_class_name) end # display the 'found it message' only if it was previously lost... unless @hwnd until @hwnd print 'unable to find the player window currently [%s] (maybe need to start program or move mouse over it)' % @name_or_regex.inspect sleep 1 STDOUT.flush hwnd = Win32::Screenshot::BitmapMaker.hwnd(@name_or_regex, @use_class_name) width, height = Win32::Screenshot::Util.dimensions_for(hwnd) p width, height @hwnd = hwnd end puts 're-established contact with window' end true end # gets the snapshot of "all the digits together" def get_bmp # Note: we no longer bring the window to the front tho...which it needs to be in both XP and Vista to work...sigh. Win32::Screenshot::BitmapMaker.capture_area(@hwnd,@x,@y,@x2,@y2) {|h,w,bmp| return bmp} end # gets snapshot of the full window def get_full_bmp Win32::Screenshot::BitmapMaker.capture_all(@hwnd) {|h,w,bmp| return bmp} end # writes out all screen tracking info to various files in the current pwd def dump_bmps filename = 'dump.bmp' File.binwrite filename, get_bmp File.binwrite 'all.' + filename, get_full_bmp dump_digits(get_digits_as_bitmaps, 'dump_bmp') if @digits end def dump_digits digits, message p "#{message} dumping digits to dump no: #{@dump_digit_count} #{Time.now.to_f}" for type, bitmap in digits File.binwrite type.to_s + '.' + @dump_digit_count.to_s + '.bmp', bitmap end File.binwrite @dump_digit_count.to_s + '.mrsh', Marshal.dump(digits) @dump_digit_count += 1 end DIGIT_TYPES = [:hours, :minute_tens, :minute_ones, :second_tens, :second_ones] # returns like {:hours => nil, :minutes_tens => raw_bmp, ... def get_digits_as_bitmaps # @digits are like {:hours => [100,5], :minute_tens => [x, width], :minute_ones, :second_tens, :second_ones} out = {} for type in DIGIT_TYPES assert @digits.key?(type) if @digits[type] x,w = @digits[type] if(x < 0) x = @max_x + x end x2 = x + w raise 'a digit width can never be negative #{w}' if w <= 0 y2 = @y2 width = x2 - x height = y2 - @y # lodo calculate these only once... out[type] = Win32::Screenshot::BitmapMaker.capture_area(@hwnd, x, @y, x2, y2) {|h,w,bmp| bmp} end end out end def get_relative_coords_of_timestamp_window [@x,@y,@x2,@y2] end def get_coords_of_window_on_display # yea out = FFI::MemoryPointer.new(:long, 4) ScreenTracker.GetWindowRect @hwnd, out out.read_array_of_long(4) end def identify_digit bitmap OCR.identify_digit(bitmap, @digits) end # we have to wait until the next change, because when we start, it might be half-way through # the current second... def wait_till_next_change original = get_bmp time_since_last_screen_change = Time.now loop { # save away the current time to try and be most accurate... time_before_current_scan = Time.now current = get_bmp if current != original if @digits got = attempt_to_get_time_from_screen time_before_current_scan if @previously_displayed_warning && got # reassure user :) p 'tracking it successfully again' @previously_displayed_warning = false end return got else puts 'screen time change only detected... [unexpected]' # unit tests do this still return end else if(Time.now - time_since_last_screen_change > 2.0) got_implies_able_to_still_ocr = attempt_to_get_time_from_screen time_before_current_scan if got_implies_able_to_still_ocr return got_implies_able_to_still_ocr else p 'warning--unable to track screen time for some reason [perhaps screen obscured or it\'s not playing yet?] ' + @hwnd.to_s @previously_displayed_warning = true # also reget window hwnd, just in case that's the problem...(can be with VLC moving from title to title) get_hwnd_loop_forever # LODO loop through all available player descriptions to find the right one, or a changed different new one, et al end time_since_last_screen_change = Time.now end end sleep 0.02 } end def attempt_to_get_time_from_screen start_time out = {} # force it to have two matching snapshots in a row, to avoid race conditions grabbing the digits... # allow youtube to update (sigh) lodo just for utube previous = nil sleep 0.05 current = get_digits_as_bitmaps while previous != current previous = current sleep 0.05 current = get_digits_as_bitmaps # lodo it should probably poll *before* calling this, not here...maybe? end assert previous == current digits = current = previous DIGIT_TYPES.each{|type| if digits[type] digit = identify_digit(digits[type]) unless digit bitmap = digits[type] # unable to identify a digit? if $DEBUG || $VERBOSE && (type != :hours) @a ||= 1 @a += 1 @already_wrote ||= {} unless @already_wrote[bitmap] p 'unable to identify capture!' + type.to_s + @a.to_s + ' dump:' + @dump_digit_count.to_s File.binwrite("bad_digit#{@a}#{type}.bmp", bitmap) @already_wrote[bitmap] = true end end if type == :hours digit = 0 # this one can fail and that's ok in VLC bottom right else # early (failure) return return nil end else p " got digit #{type} OCR as #{digit} which was captured to dump #{@dump_digit_count - 1} #{Time.now_f}" if $DEBUG end out[type] = digit else # there isn't one specified as being on screen, so assume it is always zero (like youtube hour)... out[type] = 0 end } out = "%d:%d%d:%d%d" % DIGIT_TYPES.map{ |type| out[type] } puts '', 'got new screen time ' + out + " (+ tracking delta:" + (Time.now - start_time).to_s + ")" if $VERBOSE return out, Time.now-start_time end def process_forever_in_thread Thread.new { loop { out_time, delta = wait_till_next_change @callback.timestamp_changed out_time, delta } } end end