camera_control_dart allows you to remote control supported Canon EOS cameras wirelessly.
- Camera discovery
- Camera pairing
- Control properties like ISO, aperture, shutter speed and white balance
- Start and stop movie recording
- Image capturing
- Live view preview
- Demo mode
As of right now, camera_control_dart
is not published on pub.dev. Therefore, you may depend on it by referencing this GitHub repository or a local copy using its relative path.
dependencies:
camera_control_dart:
# Option A: Referencing the package using its GitHub repository
git:
url: https://github.com/JulianSchroden/camera_control_dart.git
ref: v0.0.2
# Option B: Referencing a local copy of the repository
path: ../camera_control_dart/
camera_control_dart
provides a builder-like initialization to configure the library to your project needs. The library does not create a singleton instance, so it is your responsibility to manage the CameraControl
instance.
final cameraControl = CameraControl.init()
.withDiscovery((discoverySetup) => discoverySetup
.withDemo()
.withEosPtpIp()
.withEosCineHttp(WifiInfoAdapterImpl()))
.withLogging(
logger: CameraControlLoggerImpl(),
enabledTopics: [
const EosPtpTransactionQueueTopic(),
const EosPtpIpDiscoveryTopic(),
]
).create();
Camera discovery is the process of scanning for nearby cameras. Within the configuration, you specify which camera types you want to discover.
- Use
withEosPtpIp()
to enable the discovery of PTP/IP Cameras by listening for UPNP advertisements. - Use
withEosCineHttp()
to check whether your phone is connected to the access point of a Canon C100 II. Since I wanted to keep the library in pure dart,withEosCineHttp
requires you to pass in an implementation of the WifiInfoAdapter interface. Check out my cine_remote project for a flutter implementation. - Use
withDemo()
if you do not own any supported camera to play around with the library using its demo implementation.
While reverse-engineering camera protocols, I had to rely heavily on debugging using logs since breakpoints pause the execution too long, causing cameras to disconnect. Therefore, I added a configurable logging infrastructure that allows you to specify the topics you are interested in.
- UnspecifiedLoggerTopic: Fallback topic to ensure logs that do not specify a
LoggerChannel
are logged. - EosPtpRawEventLoggerTopic: Raw event data logs of the GetEventData (0x9116) operation.
- EosPtpPropertyChangedLoggerTopic: Property changed event logs.
- EosPtpTransactionQueueTopic: PTP/IP transaction queue logs.
- EosPtpIpDiscoveryTopic: UPNP alive and bye-bye advertisements logs.
Use the discover
method to listen for CameraDiscoveryEvent. Whenever a camera is detected, a alive
event is emitted. On the contrary, a byebye
event is emitted when a camera disappears. Note that the Stream returned by discover
does not filter out duplicates.
final discoveryStreamSubscription =
cameraControl.discover().listen((discoveryEvent) {
// TODO: process discovery events
});
The CameraDiscoveryEventAlive contains a DiscoveryHandle with the following properties:
id
: Identifier to reference the specific camera.model
: A CameraModel instance containing a model-specificid
,name
, and the underlyingprotocol
.pairingData
: Optional PairingData instance. Only present when the camera model does not require the user to enter pairing data.
The CameraDiscoveryEventByeBye only contains the id
property and notifies that the camera is no longer available.
Pairing is only required when connecting to a Canon EOS PTP/IP camera and only when connecting for the first time.
- Navigate to your camera's
Wi-Fi function
menu. - Choose
Remote control (EOS Utility)
. - Create a new setup and follow the instructions until the message
Start paring devices
is displayed. - At this point, ensure your camera control application listens to discovery events.
- On your camera, confirm the start of the pairing procedure with
OK
. Thediscover
Stream should emit aalive
event. - Using the DiscoveryHandle of the
alive
event and the EosPtpIpCameraPairingData entered by the user, construct a CameraConnectionHandle and callcameraControl.pair(cameraHandle)
. - Confirm the pairing procedure on the camera's screen.
- Store the
PairingData
used for pairing with the camera for future connections. - Turn off the camera, wait a few seconds, and turn it back on. After the camera sends another
alive
message, you can connect using the storedPairingData
.
final cameraHandle = CameraConnectionHandle(
id: discoveryHandle.id,
model: discoveryHandle.model,
pairingData: EosPtpIpCameraPairingData(
address: discoveryHandle.address,
guid: Uint8List.fromList(List.generate(16, (index) => index)),
clientName: 'myClient',
),
);
await cameraControl.pair(cameraHandle)
// TODO: store pairingData for future connections.
When the pair
method completes without throwing, the camera pairing process is successful.
Call connect
and provide a CameraConnectionHandle to establish a connection to a camera. For camera models that do not require a pairing procedure, map the properties of a DiscoveryHandle to a CameraConnectionHandle.
Otherwise, look at the Paring section for more info.
try {
final camera = await cameraControl.connect(cameraHandle);
} catch (e) {
// Failed to connect to camera
}
When establishing a connection succeeds, the Future completes with a camera instance; otherwise, it completes with an error.
With a camera connected, you can start controlling it using the methods exposed by the Camera interface.
To ensure property updates are handled correctly, use the events()
method to listen to CameraUpdateEvents. Internally, the implementation polls for events as long as you listen to the Stream and uses the event data to update its internal PropertyCache.
final eventStreamSubscription = camera.events().listen((cameraUpdateEvent) {
// TODO: handle cameraUpdateEvent
cameraUpdateEvent.when(
descriptorChanged: (descriptor) {
// TODO: handle descriptor change
},
propValueChanged: (propType, propValue) {
// TODO: handle prop value change
},
propAllowedValuesChanged: (propType, allowedValues) {
// TODO: handle allowed values change
},
recordState: (isRecording) {
// TODO: handle record state change
},
focusMode: (focusMode) {
// TODO: handle focus mode change
},
ndFilter: (mdStops) {
// TODO: handle ND filter change
},
);
})
For image capturing, first ensure the camera has the ImageCaptureCapability
. Then call captureImage()
to take a photo.
final descriptor = await camera.getDescriptor();
if(!descriptor.hasCapability<ImageCaptureCapability>()) {
// the camera does not support capturing images
return;
}
await camera.captureImage();
To control properties like aperture, ISO, and shutter speed:
- validate that the camera has the
ControlPropCapability
- if yes, get the supported
ControlPropTypes
from the capability - with the
supportedProps
, you can request theControlProp
by callinggetProp
- the
ControlProp
contains thepropType
,currentValue
, andallowedValues
- now you can call
setProp
using the ControlPropType and one of its allowed values.
final descriptor = await camera.getDescriptor();
if (!descriptor.hasCapability<ControlPropCapability>()) {
// TODO: notify user
return;
}
// Get a list of all supported ControlPropTypes
final propCapability = descriptor.getCapability<ControlPropCapability>();
final supportedPropTypes = propCapability.supportedProps;
// Initialize a list of all supported properties including their current and allowed values
final supportedProps = <ControlProp>[];
for (final propType in supportedPropTypes) {
final controlProp = await camera.getProp(propType);
if (controlProp != null) {
supportedProps.add(controlProp);
}
}
// TODO: Dummy function to simulate some user interaction to pick a value for a type
final (propType, propValue) = await pickValue(supportedProps);
// Set the property to one of its allowed values
await camera.setProp(propType, propValue);
To acquire a Stream of Live View images, use the liveView()
method. When listening to the stream, the camera enters LiveView
Mode, and the Stream starts to emit LiveViewData
. Canceling the StreamSubscription stops the LiveView
Mode.
The LiveViewData
contains two properties:
imageBytes
: Uint8List of JPEG-encoded imageautofocusState
: represents the current autofocus state and the position of the autofocus rectangle
final liveViewStreamSubscription = camera.liveView().listen(
(liveViewData) {
const imageBytes = liveViewData.imageBytes
const autofocusState = liveViewData.autofocusState
// TODO: handle imageBytes and autofoucsState
},
);
When the LiveView
mode is enabled, you can change the autofocus position using the setAutofocusPosition()
method. The method takes a single parameter of type AutofocusPosition
that has two members, x
and y
, describing the position based on a normalized range between [0, 1]
.
// Set the autofocus rectangle to the center
await camera.setAutofocusPosition(AutofocusPosition(x: 0.5, y: 0.5));
When the camera supports the MovieRecordCapility
, you can use triggerRecord
to start/stop a movie recording. Note that the capability is only supported when the camera is in movie mode.
final descriptor = await camera.getDescriptor();
if(!descriptor.hasCapability<MovieRecordCapility>()) {
// the camera does not support recording movies
return;
}
await camera.triggerRecord();
await Future.delayed(const Duration(seconds: 5));
await camera.triggerRecord();