Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[camera_windows] Support image streams on Windows platform #7067

Merged
merged 22 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.11.1

* Added support for image streaming to `camera_windows`.
loic-sharma marked this conversation as resolved.
Show resolved Hide resolved

## 0.11.0+1

* Updates minimum supported SDK version to Flutter 3.16/Dart 3.2.
Expand Down
3 changes: 2 additions & 1 deletion packages/camera/camera/lib/src/camera_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,8 @@ class CameraController extends ValueNotifier<CameraValue> {
// TODO(bmparr): Add settings for resolution and fps.
Future<void> startImageStream(onLatestImageAvailable onAvailable) async {
assert(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS);
defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.windows);
_throwIfNotInitialized('startImageStream');
if (value.isRecordingVideo) {
throw CameraException(
Expand Down
2 changes: 1 addition & 1 deletion packages/camera/camera/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: A Flutter plugin for controlling the camera. Supports previewing
Dart.
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
version: 0.11.0+1
version: 0.11.1

environment:
sdk: ^3.2.3
Expand Down
4 changes: 4 additions & 0 deletions packages/camera/camera_windows/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.2.5

* Adds support for streaming frames.

## 0.2.4+1

* Updates to pigeon 21.
Expand Down
58 changes: 58 additions & 0 deletions packages/camera/camera_windows/lib/camera_windows.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:flutter/widgets.dart';
import 'package:stream_transform/stream_transform.dart';

import 'src/messages.g.dart';
import 'type_conversion.dart';

/// An implementation of [CameraPlatform] for Windows.
class CameraWindows extends CameraPlatform {
Expand All @@ -29,6 +30,12 @@ class CameraWindows extends CameraPlatform {
/// Camera specific method channels to allow communicating with specific cameras.
final Map<int, MethodChannel> _cameraChannels = <int, MethodChannel>{};

// The stream to receive frames from the native code.
StreamSubscription<dynamic>? _platformImageStreamSubscription;

// The stream for vending frames to platform interface clients.
StreamController<CameraImageData>? _frameStreamController;

/// The controller that broadcasts events coming from handleCameraMethodCall
///
/// It is a `broadcast` because multiple controllers will connect to
Expand Down Expand Up @@ -242,6 +249,57 @@ class CameraWindows extends CameraPlatform {
'resumeVideoRecording() is not supported due to Win32 API limitations.');
}

@override
Stream<CameraImageData> onStreamedFrameAvailable(int cameraId,
{CameraImageStreamOptions? options}) {
_installStreamController(
onListen: () => _onFrameStreamListen(cameraId),
onCancel: () => _onFrameStreamCancel(cameraId));
return _frameStreamController!.stream;
}

StreamController<CameraImageData> _installStreamController(
{void Function()? onListen, void Function()? onCancel}) {
_frameStreamController = StreamController<CameraImageData>(
onListen: onListen ?? () {},
onPause: _onFrameStreamPauseResume,
onResume: _onFrameStreamPauseResume,
onCancel: onCancel ?? () {},
);
return _frameStreamController!;
}

void _onFrameStreamListen(int cameraId) {
_startPlatformStream(cameraId);
}

Future<void> _startPlatformStream(int cameraId) async {
_startStreamListener();
await _hostApi.startImageStream(cameraId);
}

void _startStreamListener() {
const EventChannel cameraEventChannel =
EventChannel('plugins.flutter.io/camera_android/imageStream');
_platformImageStreamSubscription =
cameraEventChannel.receiveBroadcastStream().listen((dynamic imageData) {
_frameStreamController!
.add(cameraImageFromPlatformData(imageData as Map<dynamic, dynamic>));
});
}

FutureOr<void> _onFrameStreamCancel(int cameraId) async {
await _hostApi.stopImageStream(cameraId);
await _platformImageStreamSubscription?.cancel();
_platformImageStreamSubscription = null;
_frameStreamController = null;
}

void _onFrameStreamPauseResume() {
throw CameraException('InvalidCall',
'Pause and resume are not supported for onStreamedFrameAvailable');
}

@override
Future<void> setFlashMode(int cameraId, FlashMode mode) async {
// TODO(jokerttu): Implement flash mode support, https://github.com/flutter/flutter/issues/97537.
Expand Down
50 changes: 50 additions & 0 deletions packages/camera/camera_windows/lib/src/messages.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,56 @@ class CameraApi {
}
}

/// Starts the image stream for the given camera.
Future<void> startImageStream(int cameraId) async {
final String __pigeon_channelName =
'dev.flutter.pigeon.camera_windows.CameraApi.startImageStream$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList =
await __pigeon_channel.send(<Object?>[cameraId]) as List<Object?>?;
if (__pigeon_replyList == null) {
throw _createConnectionError(__pigeon_channelName);
} else if (__pigeon_replyList.length > 1) {
throw PlatformException(
code: __pigeon_replyList[0]! as String,
message: __pigeon_replyList[1] as String?,
details: __pigeon_replyList[2],
);
} else {
return;
}
}

/// Stops the image stream for the given camera.
Future<void> stopImageStream(int cameraId) async {
final String __pigeon_channelName =
'dev.flutter.pigeon.camera_windows.CameraApi.stopImageStream$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList =
await __pigeon_channel.send(<Object?>[cameraId]) as List<Object?>?;
if (__pigeon_replyList == null) {
throw _createConnectionError(__pigeon_channelName);
} else if (__pigeon_replyList.length > 1) {
throw PlatformException(
code: __pigeon_replyList[0]! as String,
message: __pigeon_replyList[1] as String?,
details: __pigeon_replyList[2],
);
} else {
return;
}
}

/// Starts the preview stream for the given camera.
Future<void> pausePreview(int cameraId) async {
final String __pigeon_channelName =
Expand Down
25 changes: 25 additions & 0 deletions packages/camera/camera_windows/lib/type_conversion.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:typed_data';

import 'package:camera_platform_interface/camera_platform_interface.dart';

/// Converts method channel call [data] for `receivedImageStreamData` to a
/// [CameraImageData].
CameraImageData cameraImageFromPlatformData(Map<dynamic, dynamic> data) {
return CameraImageData(
format: const CameraImageFormat(ImageFormatGroup.bgra8888, raw: 0),
height: data['height'] as int,
width: data['width'] as int,
lensAperture: data['lensAperture'] as double?,
sensorExposureTime: data['sensorExposureTime'] as int?,
sensorSensitivity: data['sensorSensitivity'] as double?,
planes: <CameraImagePlane>[
CameraImagePlane(
bytes: data['data'] as Uint8List,
bytesPerRow: (data['width'] as int) * 4,
)
]);
}
8 changes: 8 additions & 0 deletions packages/camera/camera_windows/pigeons/messages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ abstract class CameraApi {
@async
String stopVideoRecording(int cameraId);

/// Starts the image stream for the given camera.
@async
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these @async? It looks like the logic is synchronous.

void startImageStream(int cameraId);

/// Stops the image stream for the given camera.
@async
void stopImageStream(int cameraId);

/// Starts the preview stream for the given camera.
@async
void pausePreview(int cameraId);
Expand Down
2 changes: 1 addition & 1 deletion packages/camera/camera_windows/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: camera_windows
description: A Flutter plugin for getting information about and controlling the camera on Windows.
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_windows
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
version: 0.2.4+1
version: 0.2.5

environment:
sdk: ^3.2.0
Expand Down
2 changes: 2 additions & 0 deletions packages/camera/camera_windows/windows/camera.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ enum class PendingResultType {
kTakePicture,
kStartRecord,
kStopRecord,
kStartStream,
kStopStream,
kPausePreview,
kResumePreview,
};
Expand Down
74 changes: 74 additions & 0 deletions packages/camera/camera_windows/windows/camera_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#include "camera_plugin.h"

#include <flutter/event_channel.h>
#include <flutter/event_stream_handler_functions.h>
#include <flutter/flutter_view.h>
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
Expand Down Expand Up @@ -32,6 +34,10 @@ namespace {

const std::string kPictureCaptureExtension = "jpeg";
const std::string kVideoCaptureExtension = "mp4";
constexpr char kFrameEventChannelName[] =
"plugins.flutter.io/camera_android/imageStream";

std::unique_ptr<flutter::EventSink<flutter::EncodableValue>> event_sink;

// Builds CaptureDeviceInfo object from given device holding device name and id.
std::unique_ptr<CaptureDeviceInfo> GetDeviceInfo(IMFActivate* device) {
Expand Down Expand Up @@ -122,6 +128,22 @@ void CameraPlugin::RegisterWithRegistrar(
std::unique_ptr<CameraPlugin> plugin = std::make_unique<CameraPlugin>(
registrar->texture_registrar(), registrar->messenger());

auto frameEventchannel = std::make_unique<flutter::EventChannel<>>(
registrar->messenger(), kFrameEventChannelName,
&flutter::StandardMethodCodec::GetInstance());

auto event_channel_handler =
std::make_unique<flutter::StreamHandlerFunctions<>>(
[](auto arguments, auto events) {
event_sink = std::move(events);
return nullptr;
},
[](auto arguments) {
event_sink.reset();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to do a quick fix to the issues I raised in my previous comments, but then I noticed the code related to event_sink. This is a global, which is already very dangerous, but it's also std::moved between classes while still being referenced persistently in both. This code would likely fail the second time an image stream was started, and would absolutely fail with multiple engines.

Given the scope of issues here, I'm going to revert this. @cbracken FYI.

return nullptr;
});
frameEventchannel->SetStreamHandler(std::move(event_channel_handler));

CameraApi::SetUp(registrar->messenger(), plugin.get());

registrar->AddPlugin(std::move(plugin));
Expand Down Expand Up @@ -341,6 +363,58 @@ void CameraPlugin::StopVideoRecording(
}
}

void CameraPlugin::StartImageStream(
int64_t camera_id,
std::function<void(std::optional<FlutterError> reply)> result) {
// check if request already exists
Camera* camera = GetCameraByCameraId(camera_id);
if (!camera) {
return result(FlutterError("camera_error", "Camera not created"));
}
if (camera->HasPendingResultByType(PendingResultType::kStartStream)) {
return result(
FlutterError("camera_error", "Pending start stream request exists"));
}

if (!event_sink) {
return result(FlutterError("camera_error",
"Unable to make event channel from windows"));
}

jlundOverlay marked this conversation as resolved.
Show resolved Hide resolved
if (camera->AddPendingVoidResult(PendingResultType::kStartStream,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are the pending results added in this PR resolved? I don't see anything in the diff that retrieves them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question, when i wrote it figured id deal with that later and forgot. What exactly are these checks used for and are they necessary if the function is synchronous?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There will be no result object if the API is made synchronous.

std::move(result))) {
CaptureController* cc = camera->GetCaptureController();
assert(cc);
cc->StartImageStream(std::move(event_sink));
}
}

void CameraPlugin::StopImageStream(
int64_t camera_id,
std::function<void(std::optional<FlutterError> reply)> result) {
// check if request already exists
Camera* camera = GetCameraByCameraId(camera_id);
if (!camera) {
return result(FlutterError("camera_error", "Camera not created"));
}
if (camera->HasPendingResultByType(PendingResultType::kStartStream)) {
return result(
FlutterError("camera_error", "Pending start stream request exists"));
}
jlundOverlay marked this conversation as resolved.
Show resolved Hide resolved

if (!event_sink) {
jlundOverlay marked this conversation as resolved.
Show resolved Hide resolved
return result(FlutterError("camera_error",
"Unable to make event channel from windows"));
}

if (camera->AddPendingVoidResult(PendingResultType::kStartStream,
jlundOverlay marked this conversation as resolved.
Show resolved Hide resolved
std::move(result))) {
CaptureController* cc = camera->GetCaptureController();
assert(cc);
cc->StopImageStream();
}
}

void CameraPlugin::TakePicture(
int64_t camera_id, std::function<void(ErrorOr<std::string> reply)> result) {
auto camera = GetCameraByCameraId(camera_id);
Expand Down
6 changes: 6 additions & 0 deletions packages/camera/camera_windows/windows/camera_plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ class CameraPlugin : public flutter::Plugin,
void StopVideoRecording(
int64_t camera_id,
std::function<void(ErrorOr<std::string> reply)> result) override;
void StartImageStream(
int64_t camera_id,
std::function<void(std::optional<FlutterError> reply)> result) override;
void StopImageStream(
int64_t camera_id,
std::function<void(std::optional<FlutterError> reply)> result) override;
void TakePicture(
int64_t camera_id,
std::function<void(ErrorOr<std::string> reply)> result) override;
Expand Down
Loading