Repository URL to install this package:
Version:
0.6 ▾
|
require 'ffi/hdhomerun'
require 'pry'
#
# Documentation can be found under HDHomeRun::Tuner
#
module HDHomeRun
VERSION = "0.6"
ERROR_STRING_UNKNOWN_VARIABLE = "ERROR: unknown getset variable"
ERROR_REGEX_TUNER_LOCKED = /^ERROR: resource locked by /
ERROR_STRING_TUNER_LOCK_EXPIRED = "ERROR: lock no longer held"
MAX_TUNERS = 8
class InvalidDeviceIdError < Exception; end
class ConnectionError < Exception; end
class InvalidTunerError < Exception; end
class UnknownVariableError < Exception; end
class TunerLockedError < Exception; end
class LockExpiredError < Exception; end
class UnsupportedVersion < Exception; end
#
# Discover tuners on the network.
#
# Warning: This function will return invalid data
#
# Returns an array of hashes with the following fields:
# :id -- device id (string)
# :type -- device type (int)
# :ip_addr -- ip address (IPAddr)
# :tuner_count -- number of tuners on the device (int)
#
def self.discover
results_ptr = FFI::MemoryPointer.new(FFI::HDHomeRun::DiscoverDevice, 64)
count = FFI::HDHomeRun::discover_find_devices_custom(
0,
FFI::HDHomeRun::DEVICE_TYPE_TUNER,
FFI::HDHomeRun::DEVICE_ID_WILDCARD,
results_ptr,
64)
raise "error sending discover request" if count < 0
# Loop through the results and construct an array of hashes
# as our output.
(0..count-1).map do |i|
# construct our hash
p = FFI::HDHomeRun::DiscoverDevice.new(results_ptr[i])
out = { :id => "%08X" % p.device_id,
:type => p.device_type,
:ip_addr => p.ip_addr }
# Some versions of the library don't support tuner_count
out.merge!({ :tuner_count => p.tuner_count }) if
p.respond_to? :tuner_count
out
end
end
# Return an array of all discoverable Tuners.
def self.tuners
raise UnsupportedVersion if ENV['FFI_HDHOMERUN_OLD_DEVICE_STRUCT']
discover.map do |data|
data[:tuner_count].times.map do |tuner|
Tuner.new(data.merge(:tuner => tuner))
end
end.flatten
end
# HDHomeRun device wrapper.
class Device
include FFI::HDHomeRun
attr_reader :id
# Initialize new hdhomerun device
#
# Arguments:
# [:id] HDHomeRun id (default "FFFFFFFF")
#
# Create a device instance for any HDHomeRun device on the network:
# device = HDHomeRun::Device.new
#
# Create a device instance for a specific HDHomeRun device on the network:
# device = HDHomeRun::Device.new "FEEDFACE"
#
def initialize(p={})
p = {:id => p} unless p.is_a? Hash
p[:id] ||= "FFFFFFFF"
p[:id] = "%08X" % p[:id] if p[:id].is_a? Integer
@hd = create_from_str(p[:id].to_s, nil)
raise InvalidDeviceIdError, "Invalid device id: %s" % p[:id] \
if @hd.null?
@hd = FFI::HDHomeRun::Device.new(@hd)
@id = get_device_id_requested(@hd)
raise InvalidDeviceIdError, "Invalid device id: %08X" % @id \
unless validate_device_id(@id)
raise ConnectionError, "Unable to connect to device" \
unless get_model_str(@hd)
end
#
# Get a device variable; equivalent to:
# hdhomerun_config <id> get <key>
#
def get(key)
value = FFI::MemoryPointer.new(:pointer, 1)
error = FFI::MemoryPointer.new(:pointer, 1)
if get_var(@hd, key, value, error) < 0
raise ConnectionError,
"communication error sending request to hdhomerun device"
end
value = value.read_pointer
error = error.read_pointer
# Return the value if we didn't get an error.
return value.read_string if error.null?
# Throw a useful exception if this get was for an unknown variable.
error = error.read_string
raise UnknownVariableError, "unknown variable '%s'" % key if
error == ERROR_STRING_UNKNOWN_VARIABLE
# otherwise, it's a generic runtime error.
raise RuntimeError, error
end
#
# Set a device variable; equivalent to:
# hdhomerun_config <id> set <key> <value>
#
def set(key, value)
error = FFI::MemoryPointer.new(:pointer, 1)
if set_var(@hd, key, value.to_s, nil, error) < 0
raise ConnectionError,
"communication error sending request to hdhomerun device"
end
error = error.read_pointer
return true if error.null?
# Throw a useful exception if this get was for an unknown variable.
error = error.read_string
raise UnknownVariableError, "unknown variable '%s'" % key \
if error == ERROR_STRING_UNKNOWN_VARIABLE
# Check for locking exceptions
raise TunerLockedError, error if error =~ ERROR_REGEX_TUNER_LOCKED
raise LockExpiredError, error \
if error == ERROR_STRING_TUNER_LOCK_EXPIRED
# otherwise, it's a generic runtime error.
raise RuntimeError, error
end
# Get the number of tuners on this device.
def tuner_count
@tuner_count ||= begin
MAX_TUNERS.times do |tuner|
begin
get "/tuner%d/debug" % tuner
rescue UnknownVariableError
break tuner
end
end or raise RuntimeError, "Unable to detect tuner count"
end
end
# Get an array of tuners for this device.
def tuners
@tuners ||= tuner_count.times.map do |tuner|
Tuner.new(:id => @id, :tuner => tuner)
end
end
def id
"%08X" % @id
end
def to_s
"<%s:0x%08x @id=%s>" % [ self.class, object_id, id ]
end
end
# HDHomeRun tuner wrapper.
#
# Basic usage:
#
# # Get a tuner instance for the first tuner on the network.
# tuner = HDHomeRun::Tuner.new(:id => "FFFFFFFF", :tuner => 0)
#
# # Set the channel and program
# tuner.channel = 20
# tuner.program = 3
#
# # Open a file to write the video stream to
# File.open("capture.ts", "w") do |output|
# puts "Capturing (^C to stop):"
#
# # Read the video stream from the tuner
# tuner.capture do |video_data|
#
# # write the video data to the output file
# output.write(video_data)
#
# # Display some sort of feedback to show we're capturing.
# STDOUT.write(video_data.size > 0 ? "." : "?")
# STDOUT.flush
# end
# end
#
class Tuner < Device
extend Forwardable
include FFI::HDHomeRun
attr_reader :hd, :tuner
# Initialize new hdhomerun tuner
#
# Arguments:
# [:id] HDHomeRun id (default: "FFFFFFFF")
# [:tuner] Tuner number (default: "/tuner0")
# [:key] lockkey for tuner, see lock (default: nil)
#
# Create a tuner instance for the first tuner on any HDHomeRun box
# on the network:
# tuner => HDHomeRun::Tuner.new
#
# Create a tuner instance for the second tuner on the HDHomeRun box
# with id FE92EBD0
# tuner = HDHomeRun::Tuner.new(:id => "FE92EBD0", :tuner => 1)
#
def initialize(p={})
super(p)
@tuner = p[:tuner] || 0
@tuner = "/tuner%d" % @tuner if @tuner.is_a? Integer
self.key = p[:key] if p[:key]
# Verify the format of the tuner argument
raise InvalidTunerError, "invalid tuner: %s" % @tuner \
if set_tuner_from_str(@hd, @tuner) <= 0
# Final check to see if the tuner exists on our device.
begin
get_tuner("status")
rescue UnknownVariableError => e
raise InvalidTunerError, "invalid tuner: %s, %s" % [@tuner, e]
end
end
# Dynamically define the following getter and setter methods
%w{channel channelmap filter program target}.each do |name|
define_method name do
get_tuner(name)
end
define_method "#{name}=" do |value|
set_tuner(name, value)
end
end
# Dynamically define these as simple getter methods
%w{status streaminfo debug}.each do |name|
define_method name do
get_tuner(name)
end
end
# Do a channel scan, like +hdhomerun_config+ +scan+.
#
# Returns:
# * array FFI::HDHomeRun::Result if not given a block
# * +nil+ if given a block
def scan
lock
begin
set_tuner_target(@hd, 'none')
ptr = FFI::MemoryPointer.new(:pointer, 1)
get_tuner_channelmap(@hd, ptr)
str_ptr = ptr.read_pointer
channel_map = str_ptr.null? ? nil : str_ptr.read_string()
scan_group = get_channelmap_scan_group(channel_map)
channelscan_init(@hd, scan_group)
results = Array.new
while true
result = Result.new
break if channelscan_advance(@hd, result.pointer) <= 0
rc = channelscan_detect(@hd, result.pointer)
break if rc < 0
next if rc == 0
if block_given?
yield result
else
results << result
end
end
# ensure the tuner gets unlocked if we run in to an exception
rescue Exception
unlock
raise
end
unlock if locked?
return block_given? ? nil : results
end
##
# Get the tuner status.
#
# Returns: FFI::HDHomeRun::Status
def status
status = Status.new
get_tuner_status(@hd, nil, status)
status
end
##
# Get the tuner status.
#
# Returns: FFI::HDHomeRun::ChannelEntry
def info
channel_list_ptr = channel_list_create("us-bcast")
channel_entry_ptr = channel_list_first(channel_list_ptr)
ChannelEntry.new channel_entry_ptr
end
##
# Gets the program streams for the current channel.
#
# Returns: an array of strings
def programs
pstreaminfo_ptr = FFI::MemoryPointer.new(:pointer, 1)
get_tuner_streaminfo(@hd, pstreaminfo_ptr)
pstreaminfo_str = pstreaminfo_ptr.read_pointer
if pstreaminfo_str.null?
return nil
else
programs = pstreaminfo_str.read_string.split "\n"
# remove tsid
programs.shift
return programs
end
end
##
# Get the current program stream.
def program
program_ptr = FFI::MemoryPointer.new(:pointer, 1)
get_tuner_program(@hd, program_ptr)
program_str = program_ptr.read_pointer
program_str.null? ? nil : program_str.read_string()
end
##
# Set the current program stream.
def program=(program)
set_tuner_program(@hd, program.to_s)
end
# Get the capture statistics for the tuner.
#
# Returns: FFI::HDHomeRun::Stats
def stats
ptr = FFI::MemoryPointer.new Stats
out = Stats.new ptr;
get_video_stats(@hd, out)
out
end
# Capture video data from the tuner.
#
# As data is received from the tuner, that data will be yielded to the
# block provided. If no data has been received from the tuner in over
# a second, capture will yield a string of length zero to the block.
#
# If the block returns [false], the tuner will stop capturing and return.
#
# Capture from the tuner, but abort if we see no data:
# tuner.capture do |buf|
# break false if buf.empty?
# end
#
def capture(p={}, &block)
raise ArgumentError, "no block" unless block_given?
raise RuntimeError, "unable to start stream" \
if stream_start(@hd) <= 0
max_delay = (p[:delay] || 0.064).to_f
last_yield = 0
begin
len_ptr = FFI::MemoryPointer.new :ulong
while true do
start = Time.now.to_f
data = stream_recv(@hd, VIDEO_DATA_BUFFER_SIZE_1S, len_ptr)
if !data.null? or start - last_yield > 1.0
last_yield = start
len = len_ptr.read_ulong
buf = data.read_string(len) if len > 0
buf ||= ""
yield(buf) or break
end
delay = max_delay - (Time.now.to_f - start)
sleep delay if delay > 0
end
ensure
stream_stop(@hd)
end
end
# Lock the tuner. Optionally a key can be provided to allow
# locking across non-persistent connections.
#
# [:k:] optional key for non-persistent connections
#
# Examples:
#
# Locking with a persistent connection:
#
# t = Tuner.new(:id => 0xFEFEFEFE, :tuner => 0)
# t.lock # => true
# t.channel = 3 # => 3
# # Create a second instance of the tuner and try to change
# # the channel.
# t2 = Tuner.new(:id => 0xFEFEFEFE, :tuner => 0)
# t2.channel = 4 # => TunerLockedError exception
# # first tuner instance releases lock
# t.unlock # => true
# # Now second tuner can make changes.
# t2.channel = 4 # => 4
#
# Locking across non-persistent connections:
#
# # Grab a tuner and lock it.
# t = Tuner.new(:id => 0xFEFEFEFE, :tuner => 0)
# t.lock 0xFFFF # => true
# t.channel = 3 # => 3
# t = nil # => nil
#
# # Create a new instance referencing the same tuner, cannot
# # set tuner variables without setting the key first.
# t2 = Tuner.new(:id => 0xFEFEFEFE, :tuner => 0)
# t2.channel = 4 # => TunerLockedError exception
# t2.key = 0xFFFF # => true
# t2.channel = 4 # => 4
# t2 = nil # => nil
#
# # tuner key can be passed in instansiation arguments.
# t3 = Tuner.new(:id => 0xFEFEFEFE, :tuner => 0, :key => 0xFFFF)
# t3.channel = 5 # => 5
#
# # Invalid keys (or expired keys) result in a LockExpiredError
# t3.key = 0x1234 # => true
# t3.channel = 6 # => LockExpiredError exception
# t3 = nil # => nil
#
def lock(k = nil)
# If no key was provided, generate a random key for this
# session.
k ||= rand(0xFFFFFFFF)
set_tuner("lockkey", k)
self.key = k
true
end
# Unlock the tuner. Requires you to already have the correct
# key set or use the [:force:] argument.
#
# Example:
# t = Tuner.new(:id => 0xFEFEFEFE, :tuner => 0)
# t2 = Tuner.new(:id => 0xFEFEFEFE, :tuner => 0)
#
# # Lock the tuner and try to unlock it without the key
# t.lock # => true
# t2.unlock # => TunerLockedError exception
# t.locked? # => true
#
# # force the tuner to unlock
# t2.unlock :force # => true
# t.locked? # => false
#
def unlock(force=false)
# FIXME uncommenting the following line causes the tests to core
# dump.
# return true unless locked?
set_tuner("lockkey", force != false ? "force" : "none")
self.key = 0
# Strange behavior here, setting the lockkey to "none" without
# the correct lockkey does not generate an error. Instead the
# new value is ignored by the tuner. So we have to check here
# to see if the tuner is still locked here to tell if our unlock
# was successful.
raise TunerLockedError if locked?
true
end
# Specify the key to use when communicating with this tuner.
def key=(key)
lockkey_use_value(@hd, key.to_i)
end
# check to see if the tuner is locked
def locked?
get_tuner("lockkey") != "none"
end
# Return the ip address that holds the lock on the tuner
def lock_owner
get_tuner("lockkey")
end
def to_s
"<%s:0x%08x @id=%s, @tuner=%s>" %
[ self.class, object_id, id, @tuner ]
end
private
def get(key)
value = FFI::MemoryPointer.new(:pointer, 1)
error = FFI::MemoryPointer.new(:pointer, 1)
if get_var(@hd, key, value, error) < 0
raise RuntimeError,
"communication error sending request to hdhomerun device"
end
value = value.read_pointer
error = error.read_pointer
raise RuntimeError, error.read_string unless error.null?
value.read_string
end
def set(key, value)
error = FFI::MemoryPointer.new(:pointer, 1)
if set_var(@hd, key, value.to_s, nil, error) < 0
raise RuntimeError,
"communication error sending request to hdhomerun device"
end
error = error.read_pointer
raise RuntimeError, error.read_string unless error.null?
true
end
def get_tuner(key)
get("%s/%s" % [@tuner, key.to_s])
end
def set_tuner(key, value)
set("%s/%s" % [@tuner, key.to_s], value)
end
end
end