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.
@@ -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