sony_camera_remote_api 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +55 -0
- data/.rspec +2 -0
- data/.simplecov +16 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +78 -0
- data/LICENSE +21 -0
- data/README.md +99 -0
- data/Rakefile +145 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/sonycam +6 -0
- data/lib/core_ext/hash_patch.rb +15 -0
- data/lib/sony_camera_remote_api/camera_api.rb +199 -0
- data/lib/sony_camera_remote_api/camera_api_group.rb +281 -0
- data/lib/sony_camera_remote_api/camera_api_group_def.rb +362 -0
- data/lib/sony_camera_remote_api/client/config.rb +266 -0
- data/lib/sony_camera_remote_api/client/main.rb +646 -0
- data/lib/sony_camera_remote_api/error.rb +41 -0
- data/lib/sony_camera_remote_api/logging.rb +76 -0
- data/lib/sony_camera_remote_api/packet.rb +109 -0
- data/lib/sony_camera_remote_api/raw_api.rb +196 -0
- data/lib/sony_camera_remote_api/scripts.rb +64 -0
- data/lib/sony_camera_remote_api/ssdp.rb +72 -0
- data/lib/sony_camera_remote_api/utils.rb +98 -0
- data/lib/sony_camera_remote_api/version.rb +3 -0
- data/lib/sony_camera_remote_api.rb +1044 -0
- data/scripts/Linux/add_ssdp_route.sh +39 -0
- data/scripts/Linux/connect_wifi.sh +114 -0
- data/scripts/connect.sh +36 -0
- data/sony_camera_remote_api.gemspec +35 -0
- metadata +231 -0
@@ -0,0 +1,1044 @@
|
|
1
|
+
require 'sony_camera_remote_api/version'
|
2
|
+
require 'sony_camera_remote_api/logging'
|
3
|
+
require 'sony_camera_remote_api/ssdp'
|
4
|
+
require 'sony_camera_remote_api/utils'
|
5
|
+
require 'sony_camera_remote_api/camera_api'
|
6
|
+
require 'sony_camera_remote_api/packet'
|
7
|
+
require 'core_ext/hash_patch'
|
8
|
+
require 'httpclient'
|
9
|
+
require 'active_support'
|
10
|
+
require 'active_support/core_ext'
|
11
|
+
require 'benchmark'
|
12
|
+
require 'forwardable'
|
13
|
+
|
14
|
+
|
15
|
+
module SonyCameraRemoteAPI
|
16
|
+
|
17
|
+
# Top-level class providing wrapper methods of Sony Camera Remote APIs.
|
18
|
+
class Camera
|
19
|
+
extend Forwardable
|
20
|
+
include Logging
|
21
|
+
include SSDP
|
22
|
+
include Utils
|
23
|
+
|
24
|
+
def_delegators :@api_manager, :apis, :method_missing, :getEvent, :getAvailableApiList,
|
25
|
+
:wait_event,
|
26
|
+
:get_parameter, :get_parameter!, :set_parameter, :set_parameter!, :get_current, :get_current!,
|
27
|
+
:support?, :support_group?
|
28
|
+
|
29
|
+
attr_reader :endpoints
|
30
|
+
|
31
|
+
# Timeout for saving images captured by continous shooting.
|
32
|
+
CONT_SHOOT_SAVING_TIME = 25
|
33
|
+
# Timeout for focusing by tracking focus.
|
34
|
+
TRACKING_FOCUS_TIMEOUT = 4
|
35
|
+
|
36
|
+
|
37
|
+
# Creates a new Camera object.
|
38
|
+
# @note It is good idea to save endpoint URLs by each cameras somewhere to omit SSDP search.
|
39
|
+
# @param [Hash] endpoints Endpoint URLs. if not given, SSDP search is executed.
|
40
|
+
# @param [Proc] reconnect_by Hook method called when Wi-Fi is disconnected.
|
41
|
+
# @param [String, IO, Array<String, IO>] log_file file name or stream to output log.
|
42
|
+
# @param [Boolean] finalize If true, stopRecMode API is called in the destructor.
|
43
|
+
# As far as I know, we don't have any trouble even if we never call stopRecMode.
|
44
|
+
def initialize(endpoints: nil, reconnect_by: nil, log_file: $stdout, finalize: false)
|
45
|
+
output_to log_file if log_file.present?
|
46
|
+
@endpoints = endpoints || ssdp_search
|
47
|
+
@reconnect_by = reconnect_by
|
48
|
+
@api_manager = CameraAPIManager.new @endpoints, reconnect_by: @reconnect_by
|
49
|
+
@cli = HTTPClient.new
|
50
|
+
@cli.connect_timeout = @cli.send_timeout = @cli.receive_timeout = 30
|
51
|
+
|
52
|
+
# Some cameras which use "Smart Remote Control" app must call this method before remote shooting.
|
53
|
+
startRecMode! timeout: 0
|
54
|
+
|
55
|
+
# As far as I know, we don't have to call stopRecMode method
|
56
|
+
# It may be useful for power-saving because stopRecMode leads to stop liveview.
|
57
|
+
if finalize
|
58
|
+
ObjectSpace.define_finalizer(self, self.class.finalize(self))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Destructor
|
63
|
+
def self.finalize(this)
|
64
|
+
proc do
|
65
|
+
this.stopRecMode!
|
66
|
+
this.log.info 'Finished remote shooting function.'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
# Change camera function to 'Remote Shooting' and then set shooting mode.
|
72
|
+
# @param [String] mode Shoot mode
|
73
|
+
# @param [String] cont Continous shooting mode (only available when shoot mode is 'still')
|
74
|
+
# @return [void]
|
75
|
+
# @see 'Shoot mode parameters' in API reference
|
76
|
+
# @see 'Continuous shooting mode parameter' in API reference
|
77
|
+
def change_function_to_shoot(mode, cont = nil)
|
78
|
+
# cameras that does not support CameraFunction API group has only 'Remote Shooting' function
|
79
|
+
set_parameter! :CameraFunction, 'Remote Shooting'
|
80
|
+
set_parameter :ShootMode, mode
|
81
|
+
if mode == 'still' && cont
|
82
|
+
set_parameter! :ContShootingMode, cont
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
# Change camera function to 'Contents Transfer'.
|
88
|
+
# You should call this method before using contents-retrieving methods, which are following 4 methods:
|
89
|
+
# * get_content_list
|
90
|
+
# * get_date_list
|
91
|
+
# * transfer_contents
|
92
|
+
# * delete_contents
|
93
|
+
# @return [void]
|
94
|
+
def change_function_to_transfer
|
95
|
+
set_parameter :CameraFunction, 'Contents Transfer'
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
# Capture still image(s) and transfer them to local storage.
|
100
|
+
# @note You have to set shooting mode to 'still' before calling this method.
|
101
|
+
# This method can be used in following continuous-shooting mode if supported:
|
102
|
+
# * Single : take a single picture
|
103
|
+
# * Burst : take 10 pictures at a time
|
104
|
+
# * MotionShot : take 10 pictures and render the movement into a single picture
|
105
|
+
# @param [Boolean] focus Flag to focus on before capturing.
|
106
|
+
# @param [Boolean] transfer Flag to transfer the postview image.
|
107
|
+
# @param [String] filename Name of image file to be transferred. If not given, original name is used.
|
108
|
+
# Only available in Single/MotionShot shooting mode.
|
109
|
+
# @param [String] prefix Prefix of sequencial image files to be transferred. If not given, original name is used.
|
110
|
+
# Only available in Burst shooting mode.
|
111
|
+
# @param [String] dir Directory where image file is saved. If not given, current directory is used.
|
112
|
+
# @return [String, Array<String>, nil] Filename of the transferred image(s). If 'transfer' is false, returns nil.
|
113
|
+
# @example
|
114
|
+
# # Capture single still image and save it as 'a.jpg' to 'image' directory
|
115
|
+
# change_function_to_shoot('still', 'Single')
|
116
|
+
# capture_still
|
117
|
+
# capture_still(filename: 'a.jpg', dir: 'image')
|
118
|
+
#
|
119
|
+
# # Capture 10 images by burst shooting mode and save them as 'TEST_0.jpg', ... 'TEST_9.jpg'.
|
120
|
+
# change_function_to_shoot('still', 'Burst')
|
121
|
+
# capture_still
|
122
|
+
# capture_still(prefix: 'TEST')
|
123
|
+
def capture_still(focus: true, transfer: true, filename: nil, prefix: nil, dir: nil)
|
124
|
+
act_focus if focus
|
125
|
+
wait_event { |r| r[1]['cameraStatus'] == 'IDLE' }
|
126
|
+
log.info 'Capturing...'
|
127
|
+
postview_url = ''
|
128
|
+
time = Benchmark.realtime do
|
129
|
+
postview_url = actTakePicture.result[0][0]
|
130
|
+
wait_event { |r| r[1]['cameraStatus'] == 'IDLE' }
|
131
|
+
end
|
132
|
+
|
133
|
+
log.debug postview_url
|
134
|
+
log.info 'Capture finished. (%.2f sec)' % [time]
|
135
|
+
|
136
|
+
if transfer
|
137
|
+
case get_current!(:ContShootingMode)
|
138
|
+
when 'Burst'
|
139
|
+
transferred = transfer_in_burst_mode postview_url, prefix: prefix, dir: dir
|
140
|
+
else
|
141
|
+
filename = File.basename(URI.parse(postview_url).path) if filename.nil?
|
142
|
+
transferred = transfer_postview(postview_url, filename, dir: dir)
|
143
|
+
end
|
144
|
+
transferred
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
# Start continuous shooting.
|
150
|
+
# To stop shooting, call stop_continuous_shooting method.
|
151
|
+
# @note You have to set shooting mode to 'still' and continuous shooting mode to following modes:
|
152
|
+
# * Continuous : take pictures continuously until stopped.
|
153
|
+
# * Spd Priority Cont. : take pictures continuously at a rate faster than 'Continuous'.
|
154
|
+
# @param [Boolean] focus Flag to focus on before capturing.
|
155
|
+
# @return [void]
|
156
|
+
# @example Do continuous shooting and transfer:
|
157
|
+
# change_function_to_shoot('still', 'Continuous')
|
158
|
+
# start_continuous_shooting
|
159
|
+
# ...
|
160
|
+
# stop_continuous_shooting(transfer: true)
|
161
|
+
def start_continuous_shooting(focus: true)
|
162
|
+
act_focus if focus
|
163
|
+
wait_event { |r| r[1]['cameraStatus'] == 'IDLE' }
|
164
|
+
startContShooting
|
165
|
+
wait_event { |r| r[1]['cameraStatus'] == 'StillCapturing' }
|
166
|
+
log.info 'Started continous shooting.'
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
# Stop continuous shooting and transfers all still images.
|
171
|
+
# @note 'transfer' flag is set false as default, because transfer time is prone to be much longer.
|
172
|
+
# @param [Boolean] transfer Flag to transfer the captured images.
|
173
|
+
# @param [String] prefix Prefix of of sequencial image files to be transferred. If not given, original name is used.
|
174
|
+
# @param [String] dir Directory where image file is saved. If not given, current directory is used.
|
175
|
+
# @return [Array<String>, nil] List of filenames of the transferred images. If 'transfer' is false, returns nil.
|
176
|
+
def stop_continuous_shooting(transfer: false, prefix: nil, dir: nil)
|
177
|
+
stopContShooting
|
178
|
+
log.info 'Stopped continuous shooting: saving...'
|
179
|
+
urls_result = wait_event(timeout: CONT_SHOOT_SAVING_TIME) { |r| r[40].present? }
|
180
|
+
urls = urls_result[40]['contShootingUrl'].map { |e| e['postviewUrl'] }
|
181
|
+
log.debug 'Got URLs.'
|
182
|
+
wait_event { |r| r[1]['cameraStatus'] == 'IDLE' }
|
183
|
+
log.info "Saving finished: #{urls.size} images."
|
184
|
+
if transfer
|
185
|
+
gen = generate_sequencial_filenames prefix, 'JPG' if prefix.present?
|
186
|
+
transferred = []
|
187
|
+
urls.each do |url|
|
188
|
+
if prefix.present?
|
189
|
+
filename = gen.next
|
190
|
+
else
|
191
|
+
filename = File.basename(URI.parse(url).path)
|
192
|
+
end
|
193
|
+
result = transfer_postview(url, filename, dir: dir)
|
194
|
+
# If transfer failed, it is possible that Wi-Fi is disconnected,
|
195
|
+
# that means subsequent postview images become unavailable.
|
196
|
+
break if result.nil?
|
197
|
+
transferred << result
|
198
|
+
end
|
199
|
+
transferred.compact
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
|
204
|
+
# Start movie recording.
|
205
|
+
# To stop recording, call stop_movie_recording method.
|
206
|
+
# @note You have to set shooting mode to 'movie' before calling this method.
|
207
|
+
# @return [void]
|
208
|
+
# @example Record movie and transfer:
|
209
|
+
# change_function_to_shoot('movie')
|
210
|
+
# start_movie_recording
|
211
|
+
# ...
|
212
|
+
# stop_movie_recording(transfer: true)
|
213
|
+
def start_movie_recording
|
214
|
+
wait_event { |r| r[1]['cameraStatus'] == 'IDLE' }
|
215
|
+
startMovieRec
|
216
|
+
wait_event { |r| r[1]['cameraStatus'] == 'MovieRecording' }
|
217
|
+
log.info 'Started movie recording.'
|
218
|
+
end
|
219
|
+
|
220
|
+
|
221
|
+
# Stop movie recording and transfers the movie file.
|
222
|
+
# @note 'transfer' flag is set false as default, because transfer time is prone to be much longer.
|
223
|
+
# @param [Boolean] transfer Flag to transfer the recorded movie file.
|
224
|
+
# @param [String] filename Name of the movie file to be transferred. If not given, original name is used.
|
225
|
+
# @param [String] dir Directory where image file is saved. If not given, current directory is used.
|
226
|
+
# @return [String, nil] Filename of the transferred movie. If 'transfer' is false, returns nil.
|
227
|
+
def stop_movie_recording(transfer: false, filename: nil, dir: nil)
|
228
|
+
stopMovieRec
|
229
|
+
wait_event { |r| r[1]['cameraStatus'] == 'IDLE' }
|
230
|
+
log.info 'Stopped movie recording.'
|
231
|
+
if transfer
|
232
|
+
transfer_recorded_movie(filename: filename, dir: dir)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
|
237
|
+
# Start interval still recording (a.k.a Timelapse).
|
238
|
+
# To stop recording, call stop_interval_recording method.
|
239
|
+
# @note You have to set shooting mode to 'intervalstill' before calling this method.
|
240
|
+
# @return [void]
|
241
|
+
# @example Do interval still recording:
|
242
|
+
# change_function_to_shoot('intervalstill')
|
243
|
+
# start_interval_recording
|
244
|
+
# ...
|
245
|
+
# stop_interval_recording(transfer: true)
|
246
|
+
def start_interval_recording
|
247
|
+
act_focus
|
248
|
+
startIntervalStillRec
|
249
|
+
wait_event { |r| r[1]['cameraStatus'] == 'IntervalRecording' }
|
250
|
+
log.info 'Started interval still recording.'
|
251
|
+
end
|
252
|
+
|
253
|
+
|
254
|
+
# Stop interval still recording and transfers all still images.
|
255
|
+
# @note 'transfer' flag is set false as default, because transfer time is prone to be much longer.
|
256
|
+
# @param [Boolean] transfer Flag to transfer still images
|
257
|
+
# @param [String] prefix Prefix of sequencial image files to be transferred. If not given, original name is used.
|
258
|
+
# @param [String] dir Directory where image file is saved. If not given, current directory is used.
|
259
|
+
# @return [Array<String>, nil] List of filenames of the transferred images. If 'transfer' is false, returns nil.
|
260
|
+
def stop_interval_recording(transfer: false, prefix: nil, dir: nil)
|
261
|
+
stopIntervalStillRec
|
262
|
+
wait_event { |r| r[1]['cameraStatus'] == 'IDLE' }
|
263
|
+
num_shots = getEvent([false]).result[58]['numberOfShots']
|
264
|
+
log.info "Stopped interval still recording: #{num_shots} images."
|
265
|
+
if transfer
|
266
|
+
transfer_interval_stills num_shots, prefix: prefix, dir: dir
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
|
271
|
+
# Start loop recording.
|
272
|
+
# To stop recording, call stop_loop_recording method.
|
273
|
+
# @note You have to set shooting mode to 'looprec' before calling this method.
|
274
|
+
# @return [void]
|
275
|
+
# @example Typical usage:
|
276
|
+
# change_function_to_shoot('looprec')
|
277
|
+
# start_loop_recording
|
278
|
+
# ...
|
279
|
+
# stop_loop_recording(transfer: true)
|
280
|
+
def start_loop_recording
|
281
|
+
wait_event { |r| r[1]['cameraStatus'] == 'IDLE' }
|
282
|
+
startLoopRec
|
283
|
+
wait_event { |r| r[1]['cameraStatus'] == 'LoopRecording' }
|
284
|
+
log.info 'Started loop recording.'
|
285
|
+
end
|
286
|
+
|
287
|
+
|
288
|
+
# Stop loop recording and transfers the movie file.
|
289
|
+
# @note 'transfer' flag is set false as default, because transfer time is prone to be much longer.
|
290
|
+
# @param [Boolean] transfer Flag to transfer the recorded movie file
|
291
|
+
# @param [String] filename Name of the movie file to be transferred. If not given, original name is used.
|
292
|
+
# @param [String] dir Directory where image file is saved. If not given, current directory is used.
|
293
|
+
# @return [String, nil] Filename of the transferred movie. If 'transfer' is false, returns nil.
|
294
|
+
def stop_loop_recording(transfer: false, filename: nil, dir: nil)
|
295
|
+
stopLoopRec
|
296
|
+
wait_event { |r| r[1]['cameraStatus'] == 'IDLE' }
|
297
|
+
log.info 'Stopped loop recording.'
|
298
|
+
if transfer
|
299
|
+
transfer_recorded_movie(filename: filename, dir: dir)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
|
304
|
+
# Act zoom.
|
305
|
+
# Zoom position can be specified by relative and absolute percentage within the range of 0-100.
|
306
|
+
# If Both option are specified, absolute position is preceded.
|
307
|
+
# @param [Fixnum] absolute Absolute position of the lense. 0 is the Wide-end and 100 is the Tele-end.
|
308
|
+
# @param [Fixnum] relative Relative percecntage to current position of the lense.
|
309
|
+
# @return [Array<Fixnum>] Array of initial zoom position and current zoom position.
|
310
|
+
# @example
|
311
|
+
# act_zoom(absolute: 0) # zoom out to the wide-end
|
312
|
+
# act_zoom(absolute: 100) # zoom in to the tele-end
|
313
|
+
# act_zoom(relative: -50) # zoom out by -50 from the current position
|
314
|
+
def act_zoom(absolute: nil, relative: nil)
|
315
|
+
# Check arguments
|
316
|
+
return if [relative, absolute].none?
|
317
|
+
relative = nil if [relative, absolute].all?
|
318
|
+
|
319
|
+
# Get current position
|
320
|
+
initial = getEvent(false).result[2]['zoomPosition']
|
321
|
+
unless initial.between? 0, 100
|
322
|
+
initial = wait_event { |r| r[2]['zoomPosition'].between? 0, 100 }[2]['zoomPosition']
|
323
|
+
end
|
324
|
+
# Return curent position if relative is 0
|
325
|
+
return initial if relative == 0
|
326
|
+
|
327
|
+
# Calculate target positions
|
328
|
+
if relative
|
329
|
+
absolute = [[initial + relative, 100].min, 0].max
|
330
|
+
else
|
331
|
+
absolute = [[absolute, 100].min, 0].max
|
332
|
+
end
|
333
|
+
relative = absolute - initial
|
334
|
+
current = initial
|
335
|
+
|
336
|
+
log.debug "Zoom started: #{initial} -> #{absolute} (relative: #{relative})"
|
337
|
+
|
338
|
+
# If absolute position is wide or tele end, use only long push zoom.
|
339
|
+
if [0, 100].include? absolute
|
340
|
+
current = zoom_until_end absolute
|
341
|
+
else
|
342
|
+
# Otherwise, use both long push and 1shot zoom by relative position
|
343
|
+
current, rest = zoom_by_long_push current, relative
|
344
|
+
current, _ = zoom_by_1shot current, rest
|
345
|
+
end
|
346
|
+
|
347
|
+
log.debug "Zoom finished: #{initial} -> #{current} (target was #{absolute})"
|
348
|
+
[initial, current]
|
349
|
+
end
|
350
|
+
|
351
|
+
|
352
|
+
# Act focus, which is the same as half-pressing shutter button.
|
353
|
+
# If already focued, this method does nothing unless 'force' parameter specified as true.
|
354
|
+
# @param [Boolean] force Re-forcus if the camera has already focused.
|
355
|
+
# @return [Boolean] +true+ if focus succeeded, +false+ if failed.
|
356
|
+
# @example
|
357
|
+
# # Try to focus on and succeeded.
|
358
|
+
# act_focus #=> true
|
359
|
+
def act_focus(force: false)
|
360
|
+
return false unless support? :actHalfPressShutter
|
361
|
+
return true unless needs_focus?(force: force)
|
362
|
+
actHalfPressShutter
|
363
|
+
rsp = wait_event { |r| ['Focused', 'Failed'].include? r[35]['focusStatus'] }
|
364
|
+
if rsp[35]['focusStatus'] =='Focused'
|
365
|
+
log.info 'Focused.'
|
366
|
+
true
|
367
|
+
elsif rsp[35]['focusStatus'] =='Failed'
|
368
|
+
log.info 'Focuse failed!'
|
369
|
+
cancelHalfPressShutter
|
370
|
+
wait_event { |r| r[35]['focusStatus'] == 'Not Focusing' }
|
371
|
+
false
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
|
376
|
+
# Act touch focus, by which we can specify the focus position.
|
377
|
+
# The focus position is expressed by percentage to the origin of coordinates, which is upper left of liveview images.
|
378
|
+
# If already focued, this method does nothing unless 'force' parameter specified as true.
|
379
|
+
# @note Tracking focus and Touch focus is exclusive function.
|
380
|
+
# Tracking focus is disabled automatically by calling this method.
|
381
|
+
# @param [Fixnum] x Percentage of X-axis position.
|
382
|
+
# @param [Fixnum] y Percentage of Y-axis position.
|
383
|
+
# @param [Boolean] force Re-forcus if the camera has already focused.
|
384
|
+
# @return [Boolean] AFType ('Touch' or 'Wide') if focus succeeded. nil if failed.
|
385
|
+
# @see Touch AF position parameter in API reference
|
386
|
+
# @example
|
387
|
+
# # Try to focus on bottom-left position and succeeded with 'Wide' type focus.
|
388
|
+
# act_touch_focus(10, 90) #=> 'Wide'
|
389
|
+
# # Try to focus on upper-right position but failed.
|
390
|
+
# act_touch_focus(90, 10) #=> nil
|
391
|
+
def act_touch_focus(x, y, force: false)
|
392
|
+
return false unless support? :setTouchAFPosition
|
393
|
+
return true unless needs_focus?(force: force)
|
394
|
+
set_parameter! :TrackingFocus, 'Off'
|
395
|
+
|
396
|
+
x = [[x, 100].min, 0].max
|
397
|
+
y = [[y, 100].min, 0].max
|
398
|
+
result = setTouchAFPosition([x, y]).result
|
399
|
+
if result[1]['AFResult'] == true
|
400
|
+
log.info "Touch focus (#{x}, #{y}) OK."
|
401
|
+
# result[1]['AFType']
|
402
|
+
true
|
403
|
+
else
|
404
|
+
log.info "Touch focus (#{x}, #{y}) failed."
|
405
|
+
false
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
|
410
|
+
# Act trackig focus, by which the focus position automatically track the object.
|
411
|
+
# The focus position is expressed by percentage to the origin of coordinates, which is upper left of liveview images.
|
412
|
+
# If already focued, this method does nothing unless 'force' parameter specified as true.
|
413
|
+
# @param [Fixnum] x Percentage of X-axis position.
|
414
|
+
# @param [Fixnum] y Percentage of Y-axis position.
|
415
|
+
# @param [Boolean] force Re-forcus if the camera has already focused.
|
416
|
+
# @return [Boolean] +true+ if focus succeeded, +false+ if failed.
|
417
|
+
# @example
|
418
|
+
# # Act tracking focus from the center position, and succeeded to start tracking.
|
419
|
+
# act_tracking_focus(50, 50) #=> true
|
420
|
+
def act_tracking_focus(x, y, force: false)
|
421
|
+
return false unless support_group? :TrackingFocus
|
422
|
+
return true unless needs_focus?(force: force)
|
423
|
+
set_parameter :TrackingFocus, 'On'
|
424
|
+
|
425
|
+
x = [[x, 100].min, 0].max
|
426
|
+
y = [[y, 100].min, 0].max
|
427
|
+
actTrackingFocus(['xPosition': x, 'yPosition': y]).result
|
428
|
+
begin
|
429
|
+
wait_event(timeout: TRACKING_FOCUS_TIMEOUT) { |r| r[54]['trackingFocusStatus'] == 'Tracking' }
|
430
|
+
log.info "Tracking focus (#{x}, #{y}) OK."
|
431
|
+
true
|
432
|
+
rescue EventTimeoutError => e
|
433
|
+
log.info "Tracking focus (#{x}, #{y}) Failed."
|
434
|
+
false
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
|
439
|
+
# Return whether the camera has focused or not.
|
440
|
+
# @return [Boolean] +true+ if focused, +false+ otherwise.
|
441
|
+
def focused?
|
442
|
+
result = getEvent(false).result
|
443
|
+
result[35] && result[35]['focusStatus'] == 'Focused'
|
444
|
+
end
|
445
|
+
|
446
|
+
|
447
|
+
# Cancel all type of focuses (half press, touch focus, tracking focus).
|
448
|
+
# @return [void]
|
449
|
+
def cancel_focus
|
450
|
+
result = getEvent(false).result
|
451
|
+
# Canceling tracking/touch focus should be preceded for half-press
|
452
|
+
if result[54] && result[54]['trackingFocusStatus'] == 'Tracking'
|
453
|
+
cancelTrackingFocus
|
454
|
+
rsp = wait_event { |r| r[54]['trackingFocusStatus'] == 'Not Tracking' }
|
455
|
+
end
|
456
|
+
if result[34] && result[34]['currentSet'] == true
|
457
|
+
cancelTouchAFPosition
|
458
|
+
rsp = wait_event { |r| r[34]['currentSet'] == false }
|
459
|
+
end
|
460
|
+
if result[35] && result[35]['focusStatus'] != 'Not Focusing'
|
461
|
+
cancelHalfPressShutter
|
462
|
+
rsp = wait_event { |r| r[35]['focusStatus'] == 'Not Focusing' }
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
|
467
|
+
|
468
|
+
# Starts a new thread that downloads streamed liveview images.
|
469
|
+
# This liveview thread continues downloading unless the one of the following conditions meets:
|
470
|
+
# The both hook method is called called each time after a liveview image or frame is downloaded.
|
471
|
+
# @param [String] size The liveview size.
|
472
|
+
# @param [Fixnum] time Time in seconds until finishing liveview streaming.
|
473
|
+
# @yield [LiveviewImage, LiveviewFrameInformation] The block called every time a liveview image is downloaded.
|
474
|
+
# @yieldparam [LiveviewImage] liveview image of each frame.
|
475
|
+
# @yieldparam [LiveviewFrameInformation] liveview frame information of each frame.
|
476
|
+
# If liveview frame information is not supported, nil is always given.
|
477
|
+
# @return [Thread] liveview downloading thread object
|
478
|
+
def start_liveview_thread(size: nil, time: nil)
|
479
|
+
liveview_url = init_liveview size: size
|
480
|
+
log.debug "liveview URL: #{liveview_url}"
|
481
|
+
|
482
|
+
th = Thread.new do
|
483
|
+
thread_start = loop_end = Time.now
|
484
|
+
count = 0
|
485
|
+
buffer = ''
|
486
|
+
frame_info= nil
|
487
|
+
# Ensure to finalize if the thread is killed
|
488
|
+
begin
|
489
|
+
# Break from loop inside when timeout
|
490
|
+
catch :finished do
|
491
|
+
# For reconnection
|
492
|
+
reconnect_and_retry_forever do
|
493
|
+
# Retrieve streaming data
|
494
|
+
@cli.get_content(liveview_url) do |chunk|
|
495
|
+
loop_start = Time.now
|
496
|
+
received_sec = loop_start - loop_end
|
497
|
+
|
498
|
+
buffer << chunk
|
499
|
+
log.debug "start--------------------buffer.size=#{buffer.size}, #{format("%.2f", received_sec * 1000)} ms"
|
500
|
+
begin
|
501
|
+
obj = LiveviewPacket.read(buffer)
|
502
|
+
rescue EOFError => e
|
503
|
+
# Simply read more data
|
504
|
+
rescue IOError, BinData::ValidityError => e
|
505
|
+
# Clear buffer and read data again
|
506
|
+
buffer = ''
|
507
|
+
else
|
508
|
+
# Received an packet successfully!
|
509
|
+
case obj.payload_type
|
510
|
+
when 0x01
|
511
|
+
# When payload is jpeg data
|
512
|
+
log.debug " sequence : #{obj.sequence_number}"
|
513
|
+
log.debug " data_size : #{obj.payload.payload_data_size_wo_padding}"
|
514
|
+
log.debug " pad_size : #{obj.payload.padding_size}"
|
515
|
+
block_time = Benchmark.realtime do
|
516
|
+
yield(LiveviewImage.new(obj), frame_info)
|
517
|
+
end
|
518
|
+
log.info "block time : #{format('%.2f', block_time*1000)} ms."
|
519
|
+
count += 1
|
520
|
+
when 0x02
|
521
|
+
# When payload is liveview frame information
|
522
|
+
log.info "frame count = #{obj.payload.frame_count}"
|
523
|
+
if obj.payload.frame_count > 0
|
524
|
+
obj.payload.frame_data.each do |d|
|
525
|
+
log.debug " category : #{d.category}"
|
526
|
+
log.debug " status : #{d.status}, #{d.additional_status}"
|
527
|
+
log.debug " top-left : #{d.top_left.x}, #{d.top_left.y}"
|
528
|
+
log.debug " bottom-right : #{d.bottom_right.x}, #{d.bottom_right.y}"
|
529
|
+
end
|
530
|
+
end
|
531
|
+
# Keep until next liveview image comes.
|
532
|
+
frame_info = LiveviewFrameInformation.new obj
|
533
|
+
end
|
534
|
+
|
535
|
+
last_loop_end = loop_end
|
536
|
+
loop_end = Time.now
|
537
|
+
loop_elapsed = loop_end - last_loop_end
|
538
|
+
log.debug "end----------------------#{format("%.2f", loop_elapsed * 1000)} ms, #{format("%.2f", 1 / loop_elapsed)} fps"
|
539
|
+
|
540
|
+
# Delete the packet data from buffer
|
541
|
+
buffer = buffer[obj.num_bytes..-1]
|
542
|
+
|
543
|
+
# Finish if time exceeds total elapsed time
|
544
|
+
throw :finished if time && (loop_end - thread_start > time)
|
545
|
+
end
|
546
|
+
end
|
547
|
+
end
|
548
|
+
end
|
549
|
+
ensure
|
550
|
+
# Comes here when liveview finished or killed by signal
|
551
|
+
puts 'Stopping Liveview...'
|
552
|
+
stopLiveview
|
553
|
+
total_time = Time.now - thread_start
|
554
|
+
log.info 'Liveview thread finished.'
|
555
|
+
log.debug " total time: #{format('%d', total_time)} sec"
|
556
|
+
log.debug " count: #{format('%d', count)} frames"
|
557
|
+
log.debug " rate: #{format('%.2f', count/total_time)} fps"
|
558
|
+
end
|
559
|
+
end
|
560
|
+
th
|
561
|
+
end
|
562
|
+
|
563
|
+
|
564
|
+
# Get a list of content information.
|
565
|
+
# Content information is Hash object that contains URI, file name, timestamp and other informations.
|
566
|
+
# You can transfer contents by calling 'transfer_contents' method with the content information Hash.
|
567
|
+
# This is basically the wrapper of getContentList API. For more information about request/response, see API reference.
|
568
|
+
# @note You have to set camera function to 'Contents Transfer' before calling this method.
|
569
|
+
# @param [String, Array<String>] type Same as 'type' request parameter of getContentList API.
|
570
|
+
# @param [Boolean] date Date in format of 'YYYYMMDD' used in date-view. If not specified, flat-view is used.
|
571
|
+
# @param [String] sort Same as 'sort' request parameter of getContentList API.
|
572
|
+
# @param [Fixnum] count Number of contents to get.
|
573
|
+
# Unlike the one of request parameter of getContentList API, you can specify over 100.
|
574
|
+
# @return [Array<Hash>] Content informations
|
575
|
+
# @see getContentList API in the API reference.
|
576
|
+
# @example Typical usage:
|
577
|
+
# change_function_to_transfer
|
578
|
+
#
|
579
|
+
# # Get all contents in the storage
|
580
|
+
# get_content_list
|
581
|
+
# # Get still contents captured on 2016/8/1
|
582
|
+
# get_content_list(type: 'still', date: '20160801')
|
583
|
+
# # Get 3 oldest XAVC-S movie contents
|
584
|
+
# get_content_list(type: 'movie_xavcs', sort: 'ascending', count: 3)
|
585
|
+
def get_content_list(type: nil, date: nil, sort: 'descending', count: nil)
|
586
|
+
type = Array(type) if type.is_a? String
|
587
|
+
|
588
|
+
scheme = getSchemeList.result[0][0]['scheme']
|
589
|
+
source = getSourceList([{'scheme' => scheme}]).result[0][0]['source']
|
590
|
+
|
591
|
+
if date
|
592
|
+
date_found, cnt = get_date_list.find { |d| d[0]['title'] == date }
|
593
|
+
if date_found
|
594
|
+
contents = get_content_list_sub date_found['uri'], type: type, view: 'date', sort: sort, count: count
|
595
|
+
else
|
596
|
+
log.error "Cannot find any contents at date '#{date}'!"
|
597
|
+
return []
|
598
|
+
end
|
599
|
+
else
|
600
|
+
# type option is available ONLY FOR 'date' view.
|
601
|
+
if type.present?
|
602
|
+
# if 'type' option is specified, call getContentList with date view for every date.
|
603
|
+
# this is because getContentList with flat view is extremely slow as a number of contents grows.
|
604
|
+
dates_counts = get_date_list type: type, sort: sort
|
605
|
+
contents = []
|
606
|
+
if count.present?
|
607
|
+
dates_counts.each do |date, cnt_of_date|
|
608
|
+
num = [cnt_of_date, count - contents.size].min
|
609
|
+
contents += get_content_list_sub date['uri'], type: type, view: 'date', sort: sort, count: num
|
610
|
+
break if contents.size >= count
|
611
|
+
end
|
612
|
+
# it is no problem that a number of contents is less than count
|
613
|
+
contents = contents[0, count]
|
614
|
+
else
|
615
|
+
dates_counts.each do |date, cnt_of_date|
|
616
|
+
contents += get_content_list_sub date['uri'], type: type, view: 'date', sort: sort, count: cnt_of_date
|
617
|
+
end
|
618
|
+
end
|
619
|
+
else
|
620
|
+
# contents = get_content_list_sub source, view: 'flat', sort: sort, count: count
|
621
|
+
contents = get_content_list_sub source, view: 'flat', sort: sort, count: count
|
622
|
+
end
|
623
|
+
end
|
624
|
+
contents
|
625
|
+
end
|
626
|
+
|
627
|
+
|
628
|
+
# Gets a list of dates and the number of contents of each date.
|
629
|
+
# This is basically the wrapper of getContentList API. For more information about request/response, see API reference.
|
630
|
+
# @note You have to set camera function to 'Contents Transfer' before calling this method.
|
631
|
+
# @param [String, Array<String>] type Same as 'type' request parameter of getContentList API
|
632
|
+
# @param [String] sort Same as 'sort' request parameter of getContentList API
|
633
|
+
# @param [Fixnum] date_count Number of dates to get.
|
634
|
+
# @param [Fixnum] content_count Number of contents to get
|
635
|
+
# @return [Array< Array<String, Fixnum> >] Array of pairs of a date in format of 'YYYYMMDD' and a number of contents of the date.
|
636
|
+
# @example Typical usage:
|
637
|
+
# change_function_to_transfer
|
638
|
+
# # Get all dates and the number of contents of the date
|
639
|
+
# get_date_list
|
640
|
+
# # Get 5 newest dates that contains at least one MP4 and XAVC-S movie content.
|
641
|
+
# get_date_list(type: ['movie_mp4','movie_xavcs'], date_count: 5)
|
642
|
+
# # Get all dates that contains at least 10 still contents.
|
643
|
+
# get_date_list(type: 'still', content_count: 10)
|
644
|
+
def get_date_list(type: nil, sort: 'descending', date_count: nil, content_count: nil)
|
645
|
+
type = Array(type) if type.is_a? String
|
646
|
+
|
647
|
+
scheme = getSchemeList.result[0][0]['scheme']
|
648
|
+
source = getSourceList([{'scheme' => scheme}]).result[0][0]['source']
|
649
|
+
|
650
|
+
if type.present?
|
651
|
+
# If type is specifid, get all dates and check the count of contents type later
|
652
|
+
dates = get_content_list_sub(source, view: 'date', sort: sort)
|
653
|
+
else
|
654
|
+
# If not, simply get dates by date_count
|
655
|
+
dates = get_content_list_sub(source, view: 'date', count: date_count, sort: sort)
|
656
|
+
end
|
657
|
+
|
658
|
+
# Filter by type, date_count and content_count.
|
659
|
+
filtered_dates = []
|
660
|
+
dates.each do |d|
|
661
|
+
cnt = getContentCount([{'uri' => d['uri'], 'type' => type, 'view' => 'date'}]).result[0]['count']
|
662
|
+
# Exclude days of 0 contents.
|
663
|
+
filtered_dates << [d, cnt] if cnt > 0
|
664
|
+
# Break if contents count exceeds.
|
665
|
+
break if content_count and filtered_dates.map { |d, c| c }.inject(0, :+) > content_count
|
666
|
+
# Break if date count exceeds.
|
667
|
+
break if date_count and filtered_dates.size > date_count
|
668
|
+
end
|
669
|
+
filtered_dates
|
670
|
+
end
|
671
|
+
|
672
|
+
|
673
|
+
# Predefined transfer sizes
|
674
|
+
SIZE_LIST = %w(original large small thumbnail).freeze
|
675
|
+
# Transfer content(s) from the camera storage.
|
676
|
+
# @note You have to set camera function to 'Contents Transfer' before calling this method.
|
677
|
+
# @param [Array<Hash>] contents Array of content information, which can be obtained by get_content_list
|
678
|
+
# @param [Array<String>] filenames Array of filename strings
|
679
|
+
# @param [String] size Content size. available values are 'original', 'large', 'small', 'thumbnail'.
|
680
|
+
# @example Typical usage:
|
681
|
+
# change_function_to_transfer
|
682
|
+
# contents = get_content_list(type: 'still', count: 10) # get 10 newest still contents
|
683
|
+
# transfer_contents(contents) # transfer them
|
684
|
+
def transfer_contents(contents, filenames=[], dir: nil, size: 'original')
|
685
|
+
if SIZE_LIST.exclude?(size)
|
686
|
+
log.error "#{size} is invalid for size option!"
|
687
|
+
return nil
|
688
|
+
end
|
689
|
+
|
690
|
+
contents = [contents].compact unless contents.is_a? Array
|
691
|
+
filenames = [filenames].compact unless filenames.is_a? Array
|
692
|
+
if !filenames.empty?
|
693
|
+
if contents.size > filenames.size
|
694
|
+
log.warn 'Size of filename list is smaller than that of contents list!'
|
695
|
+
filenames += Array.new(contents.size - filenames.size, nil)
|
696
|
+
elsif contents.size < filenames.size
|
697
|
+
log.warn 'Size of filename list is bigger than that of contents list!'
|
698
|
+
end
|
699
|
+
end
|
700
|
+
|
701
|
+
urls_filenames = contents.zip(filenames).map do |content, filename|
|
702
|
+
next unless content
|
703
|
+
url =
|
704
|
+
case size
|
705
|
+
when 'original'
|
706
|
+
raise StandardError if content['content']['original'].size > 1 # FIXME: When do we come here???
|
707
|
+
content['content']['original'][0]['url']
|
708
|
+
when 'large'
|
709
|
+
content['content']['largeUrl']
|
710
|
+
when 'small'
|
711
|
+
content['content']['smallUrl']
|
712
|
+
when 'thumbnail'
|
713
|
+
content['content']['thumbnailUrl']
|
714
|
+
end
|
715
|
+
filename ||= content['content']['original'][0]['fileName']
|
716
|
+
[url, filename]
|
717
|
+
end
|
718
|
+
|
719
|
+
log.info "#{contents.size} contents to be transferred."
|
720
|
+
transferred = transfer_contents_sub(urls_filenames, dir)
|
721
|
+
if transferred.size == urls_filenames.size
|
722
|
+
log.info 'All transfer completed.'
|
723
|
+
else
|
724
|
+
log.info "Some files are failed to transfer (#{transferred.size}/#{urls_filenames.size})."
|
725
|
+
end
|
726
|
+
transferred
|
727
|
+
end
|
728
|
+
|
729
|
+
|
730
|
+
# Delete content(s) of camera storage.
|
731
|
+
# @note You have to set camera function to 'Contents Transfer' before calling this method.
|
732
|
+
# @param [Array<Hash>] contents array of content hashes, which can be obtained by get_content_list
|
733
|
+
# @example Typical usage:
|
734
|
+
# change_function_to_transfer
|
735
|
+
# contents = get_content_list(type: still, count: 10) # get 10 newest still contents
|
736
|
+
# delete_contents(contents) # delete them
|
737
|
+
def delete_contents(contents)
|
738
|
+
contents = [contents].compact unless contents.is_a? Array
|
739
|
+
count = contents.size
|
740
|
+
(0..((count - 1) / 100)).each do |i|
|
741
|
+
start = i * 100
|
742
|
+
cnt = start + 100 < count ? 100 : count - start
|
743
|
+
param = contents[start, cnt].map { |c| c['uri'] }
|
744
|
+
deleteContent [{'uri' => param}]
|
745
|
+
end
|
746
|
+
log.info "Deleted #{contents.size} contents."
|
747
|
+
end
|
748
|
+
|
749
|
+
|
750
|
+
#----------------------------------------PRIVATE METHODS----------------------------------------
|
751
|
+
|
752
|
+
private
|
753
|
+
|
754
|
+
# Transfer a postview
|
755
|
+
def transfer_postview(url, filename, dir: nil)
|
756
|
+
filepath = dir ? File.join(dir, filename) : filename
|
757
|
+
log.info "Transferring #{filepath}..."
|
758
|
+
FileUtils.mkdir_p dir if dir
|
759
|
+
result = true
|
760
|
+
time = Benchmark.realtime do
|
761
|
+
result = reconnect_and_give_up do
|
762
|
+
open(filepath, 'wb') do |file|
|
763
|
+
@cli.get_content(url) do |chunk|
|
764
|
+
file.write chunk
|
765
|
+
end
|
766
|
+
end
|
767
|
+
true
|
768
|
+
end
|
769
|
+
end
|
770
|
+
if result
|
771
|
+
log.info "Transferred #{filepath}. (#{format('%.2f', time)} sec)"
|
772
|
+
filepath
|
773
|
+
else
|
774
|
+
log.info "Failed to transfer #{filepath}. (#{format('%.2f', time)} sec)"
|
775
|
+
nil
|
776
|
+
end
|
777
|
+
end
|
778
|
+
|
779
|
+
# Use postview size parameter to determine the image size to get
|
780
|
+
def get_transfer_size
|
781
|
+
return 'original' unless support? :getPostviewImageSize
|
782
|
+
postview_size = getPostviewImageSize.result[0]
|
783
|
+
case postview_size
|
784
|
+
when 'Original'
|
785
|
+
'original'
|
786
|
+
when '2M'
|
787
|
+
'large'
|
788
|
+
end
|
789
|
+
end
|
790
|
+
|
791
|
+
|
792
|
+
# In burst shooting mode, the last one in 10 images is the only one we can get by postview URI.
|
793
|
+
# So we must change function to 'Transfer Contents', then get the newest 10 contents.
|
794
|
+
# The problem is that getContentList API can sort the results by timestamp but not by file number.
|
795
|
+
# For example, assume that you have 2 cameras A and B, and the time setting of the A is ahead of B.
|
796
|
+
# If you capture stills by A and then act burst shooting by B with the same SD card,
|
797
|
+
# you will get stills captured by A, because the 'newest' contents are determined by timestamp.
|
798
|
+
def transfer_in_burst_mode(url, prefix: nil, dir: nil)
|
799
|
+
transfer_size = get_transfer_size
|
800
|
+
change_function_to_transfer
|
801
|
+
# As of now, burst shooting mode always capture 10 still images.
|
802
|
+
contents = get_content_list type: 'still', sort: 'descending', count: 10
|
803
|
+
if prefix.present?
|
804
|
+
filenames = generate_sequencial_filenames prefix, 'JPG', num: 10
|
805
|
+
else
|
806
|
+
filenames = contents.map { |c| c['content']['original'][0]['fileName'] }
|
807
|
+
end
|
808
|
+
transferred = transfer_contents contents, filenames, size: transfer_size, dir: dir
|
809
|
+
change_function_to_shoot 'still', 'Burst'
|
810
|
+
transferred
|
811
|
+
end
|
812
|
+
|
813
|
+
|
814
|
+
def transfer_recorded_movie(filename: nil, dir: nil)
|
815
|
+
change_function_to_transfer
|
816
|
+
content = get_content_list type: ['movie_mp4', 'movie_xavcs'], sort: 'descending', count: 1
|
817
|
+
transferred = transfer_contents content, filename, dir: dir
|
818
|
+
change_function_to_shoot 'movie'
|
819
|
+
transferred[0] if !transferred.empty?
|
820
|
+
end
|
821
|
+
|
822
|
+
|
823
|
+
def transfer_interval_stills(num_shots, prefix: nil, dir: nil)
|
824
|
+
transfer_size = get_transfer_size
|
825
|
+
change_function_to_transfer
|
826
|
+
contents = get_content_list type: 'still', sort: 'descending', count: num_shots
|
827
|
+
if prefix
|
828
|
+
filenames = generate_sequencial_filenames prefix, 'JPG', num: contents.size
|
829
|
+
transferred = transfer_contents contents, filenames, dir: dir
|
830
|
+
else
|
831
|
+
transferred = transfer_contents contents, size: transfer_size, dir: dir
|
832
|
+
end
|
833
|
+
change_function_to_shoot 'intervalstill'
|
834
|
+
transferred
|
835
|
+
end
|
836
|
+
|
837
|
+
|
838
|
+
# Zoom until wide-end or tele-end.
|
839
|
+
def zoom_until_end(absolute)
|
840
|
+
case
|
841
|
+
when absolute == 100
|
842
|
+
actZoom ['in', 'start']
|
843
|
+
wait_event(polling: true) { |r| r[2]['zoomPosition'] == absolute }
|
844
|
+
actZoom ['in', 'stop']
|
845
|
+
when absolute == 0
|
846
|
+
actZoom ['out', 'start']
|
847
|
+
wait_event(polling: true) { |r| r[2]['zoomPosition'] == absolute }
|
848
|
+
actZoom ['out', 'stop']
|
849
|
+
end
|
850
|
+
absolute
|
851
|
+
end
|
852
|
+
|
853
|
+
|
854
|
+
LONG_ZOOM_THRESHOLD = 19
|
855
|
+
LONG_ZOOM_FINISH_TIMEOUT = 0.5
|
856
|
+
# Long push zoom is tend to go through the desired potision, so
|
857
|
+
# LONG_ZOOM_THRESHOLD is a important parameter.
|
858
|
+
def zoom_by_long_push(current, relative)
|
859
|
+
# Return if relative is lesser than threshold
|
860
|
+
return [current, relative] if relative.abs < LONG_ZOOM_THRESHOLD
|
861
|
+
|
862
|
+
absolute = current + relative
|
863
|
+
log.debug " Long zoom start: #{current} -> #{absolute}"
|
864
|
+
case
|
865
|
+
when relative > 0
|
866
|
+
target = absolute - LONG_ZOOM_THRESHOLD
|
867
|
+
dir = 'in'
|
868
|
+
condition = ->(r) { r[2]['zoomPosition'] > target }
|
869
|
+
when relative < 0
|
870
|
+
target = absolute + LONG_ZOOM_THRESHOLD
|
871
|
+
dir = 'out'
|
872
|
+
condition = ->(r) { r[2]['zoomPosition'] < target }
|
873
|
+
else
|
874
|
+
return [current, relative]
|
875
|
+
end
|
876
|
+
log.debug " stopping line: #{target}"
|
877
|
+
|
878
|
+
actZoom [dir, 'start']
|
879
|
+
wait_event(polling: true, &condition)
|
880
|
+
actZoom [dir, 'stop']
|
881
|
+
|
882
|
+
# Wait for the lense stops completely
|
883
|
+
final = current
|
884
|
+
loop do
|
885
|
+
begin
|
886
|
+
final = wait_event(timeout: LONG_ZOOM_FINISH_TIMEOUT) { |r| r[2]['zoomPosition'] != final }[2]['zoomPosition']
|
887
|
+
rescue EventTimeoutError => e
|
888
|
+
break
|
889
|
+
end
|
890
|
+
end
|
891
|
+
|
892
|
+
log.debug " Long zoom finished: #{current} -> #{final} (target: #{absolute})"
|
893
|
+
[final, absolute - final]
|
894
|
+
end
|
895
|
+
|
896
|
+
|
897
|
+
SHORT_ZOOM_THRESHOLD = 10
|
898
|
+
SHORT_ZOOM_FINISH_TIMEOUT = 0.5
|
899
|
+
# 1shot zoom
|
900
|
+
def zoom_by_1shot(current, relative)
|
901
|
+
# Return if relative is lesser than threshold
|
902
|
+
return [current, relative] if relative.abs < SHORT_ZOOM_THRESHOLD
|
903
|
+
|
904
|
+
absolute = current + relative
|
905
|
+
log.debug " Short zoom start: #{current} -> #{absolute}"
|
906
|
+
|
907
|
+
diff = relative
|
908
|
+
while true
|
909
|
+
if diff > 0
|
910
|
+
log.debug ' in'
|
911
|
+
actZoom ['in', '1shot']
|
912
|
+
elsif diff < 0
|
913
|
+
log.debug ' out'
|
914
|
+
actZoom ['out', '1shot']
|
915
|
+
else
|
916
|
+
break
|
917
|
+
end
|
918
|
+
pos = wait_event(polling: true) { |r| r[2]['zoomPosition'] }[2]['zoomPosition']
|
919
|
+
diff = absolute - pos
|
920
|
+
break if diff.abs < SHORT_ZOOM_THRESHOLD
|
921
|
+
end
|
922
|
+
|
923
|
+
# Wait for the lense stops completely
|
924
|
+
final = current
|
925
|
+
loop do
|
926
|
+
begin
|
927
|
+
final = wait_event(timeout: SHORT_ZOOM_FINISH_TIMEOUT) { |r| r[2]['zoomPosition'] != final }[2]['zoomPosition']
|
928
|
+
rescue EventTimeoutError => e
|
929
|
+
break
|
930
|
+
end
|
931
|
+
end
|
932
|
+
|
933
|
+
log.debug " Short zoom finished: #{current} -> #{final} (target: #{absolute})"
|
934
|
+
[final, absolute - final]
|
935
|
+
end
|
936
|
+
|
937
|
+
|
938
|
+
def needs_focus?(force: false)
|
939
|
+
if force
|
940
|
+
cancel_focus
|
941
|
+
true
|
942
|
+
else
|
943
|
+
! focused?
|
944
|
+
end
|
945
|
+
end
|
946
|
+
|
947
|
+
|
948
|
+
# Initialize and start liveview
|
949
|
+
def init_liveview(size: nil)
|
950
|
+
# Enable liveview frame information if available
|
951
|
+
setLiveviewFrameInfo!([{'frameInfo' => true}])
|
952
|
+
|
953
|
+
if size
|
954
|
+
# need to stop liveview when the liveview size is changed
|
955
|
+
stopLiveview
|
956
|
+
current, available = getAvailableLiveviewSize.result
|
957
|
+
unless available.include?(size)
|
958
|
+
raise IllegalArgument, new, "The value '#{size}' is not available for parameter 'LiveviewSize'. current: #{current}, available: #{available}"
|
959
|
+
end
|
960
|
+
startLiveviewWithSize([size]).result[0]
|
961
|
+
else
|
962
|
+
startLiveview.result[0]
|
963
|
+
end
|
964
|
+
end
|
965
|
+
|
966
|
+
|
967
|
+
def get_content_list_sub(source, type: nil, target: 'all', view:, sort:, count: nil)
|
968
|
+
max_count = getContentCount([{'uri' => source, 'type' => type, 'view' => view}]).result[0]['count']
|
969
|
+
count = count ? [max_count, count].min : max_count
|
970
|
+
contents = []
|
971
|
+
(0..((count - 1) / 100)).each do |i|
|
972
|
+
start = i * 100
|
973
|
+
cnt = start + 100 < count ? 100 : count - start
|
974
|
+
contents += getContentList([{'uri' => source, 'stIdx' => start, 'cnt' => cnt, 'type' => type, 'view' => view, 'sort' => sort}]).result[0]
|
975
|
+
# pp contents
|
976
|
+
end
|
977
|
+
contents
|
978
|
+
end
|
979
|
+
|
980
|
+
|
981
|
+
def transfer_contents_sub(urls_filenames, dir)
|
982
|
+
FileUtils.mkdir_p dir if dir
|
983
|
+
transferred = []
|
984
|
+
urls_filenames.each do |url, filename|
|
985
|
+
next unless url
|
986
|
+
filepath = dir ? File.join(dir, filename) : filename
|
987
|
+
log.debug url
|
988
|
+
log.info "Transferring #{filepath}..."
|
989
|
+
time = Benchmark.realtime do
|
990
|
+
reconnect_and_retry(hook: method(:change_function_to_transfer)) do
|
991
|
+
open(filepath, 'wb') do |file|
|
992
|
+
@cli.get_content(url) do |chunk|
|
993
|
+
file.write chunk
|
994
|
+
end
|
995
|
+
end
|
996
|
+
end
|
997
|
+
end
|
998
|
+
log.info "Transferred #{filepath}. (#{format('%.2f', time)} sec)"
|
999
|
+
transferred << filepath
|
1000
|
+
end
|
1001
|
+
transferred
|
1002
|
+
end
|
1003
|
+
|
1004
|
+
|
1005
|
+
# Try to run given block.
|
1006
|
+
# If an error raised because of the Wi-Fi disconnection, try to reconnect and run the block again.
|
1007
|
+
# If error still continues, give it up and raise the error.
|
1008
|
+
def reconnect_and_retry(retrying: true, num: 1, hook: nil)
|
1009
|
+
yield
|
1010
|
+
rescue HTTPClient::TimeoutError, Errno::EHOSTUNREACH, Errno::ECONNREFUSED => e
|
1011
|
+
retry_count ||= 0
|
1012
|
+
raise e if @reconnect_by.nil? || retry_count >= num
|
1013
|
+
log.error "#{e.class}: #{e.message}"
|
1014
|
+
log.error 'The camera seems to be disconnected! Reconnecting...'
|
1015
|
+
unless @reconnect_by.call
|
1016
|
+
log.error 'Failed to reconnect.'
|
1017
|
+
raise e
|
1018
|
+
end
|
1019
|
+
log.error 'Reconnected.'
|
1020
|
+
@cli.reset_all
|
1021
|
+
# For cameras that use Smart Remote Control app.
|
1022
|
+
startRecMode! timeout: 0
|
1023
|
+
|
1024
|
+
if hook
|
1025
|
+
unless hook.call
|
1026
|
+
log.error 'Before-retry hook failed.'
|
1027
|
+
raise e
|
1028
|
+
end
|
1029
|
+
end
|
1030
|
+
retry_count += 1
|
1031
|
+
retry if retrying
|
1032
|
+
end
|
1033
|
+
|
1034
|
+
def reconnect_and_retry_forever(&block)
|
1035
|
+
reconnect_and_retry &block
|
1036
|
+
rescue HTTPClient::TimeoutError, Errno::EHOSTUNREACH, Errno::ECONNREFUSED => e
|
1037
|
+
retry
|
1038
|
+
end
|
1039
|
+
|
1040
|
+
def reconnect_and_give_up(&block)
|
1041
|
+
reconnect_and_retry(retrying: false, &block)
|
1042
|
+
end
|
1043
|
+
end
|
1044
|
+
end
|