-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Add a camera feed demo in misc #1225
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
Open
shiena
wants to merge
5
commits into
godotengine:master
Choose a base branch
from
shiena:feature/camerafeed-demo
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
# Camera Feed Demo | ||
|
||
A demo that shows how to display live camera feeds from various sources | ||
using Godot's [CameraFeed](https://docs.godotengine.org/en/stable/classes/class_camerafeed.html) | ||
and [CameraServer](https://docs.godotengine.org/en/stable/classes/class_cameraserver.html) APIs. | ||
Supports multiple platforms including desktop, mobile, and web browsers. | ||
|
||
Language: GDScript | ||
|
||
Renderer: Compatibility, Mobile, Forward+ | ||
|
||
> Note: this demo requires Godot 4.5 or later | ||
|
||
# How does it work? | ||
|
||
The demo uses `CameraServer` to enumerate available camera devices and display their feeds in real-time. Key features include: | ||
|
||
1. **Camera Detection**: Automatically detects all available camera feeds using `CameraServer.feeds()`. | ||
|
||
2. **Platform Support**: | ||
- Handles camera permissions on mobile platforms (Android/iOS) | ||
- Supports web browsers with special monitoring setup | ||
- Works on desktop platforms with standard camera APIs | ||
|
||
3. **Feed Formats**: | ||
- RGB format for standard color feeds | ||
- YCbCr format with shader-based conversion for certain devices | ||
- Dynamic format selection based on camera capabilities | ||
|
||
4. **Real-time Display**: | ||
- Uses `CameraTexture` to display live camera feeds | ||
- Handles camera rotation and orientation transforms | ||
- Maintains proper aspect ratio for different camera resolutions | ||
|
||
5. **Shader Processing**: | ||
- Custom shader (`ycbcr_to_rgb.gdshader`) converts YCbCr feeds to RGB | ||
- Uses BT.709 color space conversion standard for HDTV | ||
|
||
The UI provides controls to select cameras, choose formats, and start/stop the feed display. | ||
|
||
## Screenshots | ||
|
||
 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
extends Control | ||
|
||
@onready var camera_display := $CameraDisplay | ||
@onready var camera_preview := $CameraDisplay/CameraPreview | ||
@onready var camera_list := $DrawerContainer/Drawer/DrawerContent/VBoxContainer/CameraList | ||
@onready var format_list := $DrawerContainer/Drawer/DrawerContent/VBoxContainer/FormatList | ||
@onready var start_or_stop_button := $DrawerContainer/Drawer/DrawerContent/VBoxContainer/ButtonContainer/StartOrStopButton | ||
@onready var reload_button := $DrawerContainer/Drawer/DrawerContent/VBoxContainer/ButtonContainer/ReloadButton | ||
|
||
var camera_feed: CameraFeed | ||
|
||
const defaultWebResolution: Dictionary = { | ||
"width": 640, | ||
"height": 480, | ||
} | ||
|
||
func _ready() -> void: | ||
_adjust_ui() | ||
_reload_camera_list() | ||
|
||
|
||
func _adjust_ui() -> void: | ||
camera_display.size = camera_display.get_parent_area_size() - Vector2.ONE * 40 | ||
camera_preview.custom_minimum_size = camera_display.size | ||
camera_preview.position = camera_display.size / 2 | ||
|
||
|
||
func _reload_camera_list() -> void: | ||
camera_list.clear() | ||
format_list.clear() | ||
|
||
var os_name := OS.get_name() | ||
# Request camera permission on mobile. | ||
if os_name in ["Android", "iOS"]: | ||
var permissions = OS.get_granted_permissions() | ||
if not "CAMERA" in permissions: | ||
if not OS.request_permission("CAMERA"): | ||
print("CAMERA permission not granted") | ||
return | ||
|
||
if not CameraServer.camera_feeds_updated.is_connected(_on_camera_feeds_updated): | ||
CameraServer.camera_feeds_updated.connect(_on_camera_feeds_updated) | ||
|
||
if CameraServer.monitoring_feeds: | ||
AThousandShips marked this conversation as resolved.
Show resolved
Hide resolved
|
||
CameraServer.monitoring_feeds = false | ||
await get_tree().process_frame | ||
|
||
CameraServer.monitoring_feeds = true | ||
AThousandShips marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
func _on_camera_feeds_updated() -> void: | ||
# Get available camera feeds. | ||
var feeds = CameraServer.feeds() | ||
if feeds.is_empty(): | ||
camera_list.add_item("No cameras found") | ||
camera_list.disabled = true | ||
format_list.add_item("No formats available") | ||
format_list.disabled = true | ||
start_or_stop_button.disabled = true | ||
return | ||
|
||
camera_list.disabled = false | ||
for i in feeds.size(): | ||
var feed: CameraFeed = feeds[i] | ||
camera_list.add_item(feed.get_name()) | ||
|
||
# Auto-select first camera. | ||
camera_list.selected = 0 | ||
_on_camera_list_item_selected(0) | ||
|
||
|
||
func _on_camera_list_item_selected(index: int) -> void: | ||
var camera_feeds := CameraServer.feeds() | ||
if index < 0 or index >= camera_feeds.size(): | ||
return | ||
|
||
# Stop previous camera if active. | ||
if camera_feed and camera_feed.feed_is_active: | ||
camera_feed.feed_is_active = false | ||
|
||
# Get selected camera feed. | ||
camera_feed = camera_feeds[index] | ||
|
||
# Update format list. | ||
_update_format_list() | ||
|
||
|
||
func _update_format_list() -> void: | ||
format_list.clear() | ||
|
||
if not camera_feed: | ||
return | ||
|
||
var formats = camera_feed.get_formats() | ||
if formats.is_empty(): | ||
format_list.add_item("No formats available") | ||
format_list.disabled = true | ||
start_or_stop_button.disabled = true | ||
return | ||
|
||
format_list.disabled = false | ||
for format in formats: | ||
var resolution := str(format["width"]) + "x" + str(format["height"]) | ||
var item := "%s - %s" % [format["format"], resolution] | ||
if OS.get_name() == "Windows": | ||
item += " : %s / %s" % [format["frame_denominator"], format["frame_numerator"]] | ||
format_list.add_item(item) | ||
|
||
# Auto-select first format. | ||
format_list.selected = 0 | ||
_on_format_list_item_selected(0) | ||
|
||
|
||
func _on_format_list_item_selected(index: int) -> void: | ||
if not camera_feed: | ||
return | ||
|
||
var formats := camera_feed.get_formats() | ||
if index < 0 or index >= formats.size(): | ||
return | ||
var os_name := OS.get_name() | ||
var parameters: Dictionary = defaultWebResolution if os_name == "Web" else {} | ||
camera_feed.set_format(index, parameters) | ||
_start_camera_feed() | ||
|
||
|
||
func _start_camera_feed() -> void: | ||
if not camera_feed: | ||
return | ||
|
||
if not camera_feed.frame_changed.is_connected(_on_frame_changed): | ||
camera_feed.frame_changed.connect(_on_frame_changed, ConnectFlags.CONNECT_ONE_SHOT) | ||
# Start the feed. | ||
camera_feed.feed_is_active = true | ||
|
||
|
||
func _on_frame_changed() -> void: | ||
var datatype := camera_feed.get_datatype() as CameraFeed.FeedDataType | ||
var preview_size := Vector2.ZERO | ||
|
||
var mat: ShaderMaterial = camera_preview.material | ||
AThousandShips marked this conversation as resolved.
Show resolved
Hide resolved
|
||
var rgb_texture: CameraTexture = mat.get_shader_parameter("rgb_texture") | ||
var y_texture: CameraTexture = mat.get_shader_parameter("y_texture") | ||
var cbcr_texture: CameraTexture = mat.get_shader_parameter("cbcr_texture") | ||
var ycbcr_texture: CameraTexture = mat.get_shader_parameter("ycbcr_texture") | ||
|
||
rgb_texture.which_feed = CameraServer.FeedImage.FEED_RGBA_IMAGE | ||
AThousandShips marked this conversation as resolved.
Show resolved
Hide resolved
|
||
y_texture.which_feed = CameraServer.FeedImage.FEED_Y_IMAGE | ||
cbcr_texture.which_feed = CameraServer.FeedImage.FEED_CBCR_IMAGE | ||
ycbcr_texture.which_feed = CameraServer.FEED_YCBCR_IMAGE | ||
|
||
match datatype: | ||
AThousandShips marked this conversation as resolved.
Show resolved
Hide resolved
|
||
CameraFeed.FeedDataType.FEED_RGB: | ||
rgb_texture.camera_feed_id = camera_feed.get_id() | ||
mat.set_shader_parameter("rgb_texture", rgb_texture) | ||
mat.set_shader_parameter("mode", 0) | ||
preview_size = rgb_texture.get_size() | ||
CameraFeed.FeedDataType.FEED_YCBCR_SEP: | ||
y_texture.camera_feed_id = camera_feed.get_id() | ||
cbcr_texture.camera_feed_id = camera_feed.get_id() | ||
mat.set_shader_parameter("y_texture", y_texture) | ||
mat.set_shader_parameter("cbcr_texture", cbcr_texture) | ||
mat.set_shader_parameter("mode", 1) | ||
preview_size = y_texture.get_size() | ||
CameraFeed.FeedDataType.FEED_YCBCR: | ||
ycbcr_texture.camera_feed_id = camera_feed.get_id() | ||
mat.set_shader_parameter("ycbcr_texture", ycbcr_texture) | ||
mat.set_shader_parameter("mode", 2) | ||
preview_size = ycbcr_texture.get_size() | ||
_: | ||
print("Skip formats that are not supported.") | ||
return | ||
|
||
var white_image := Image.create(int(preview_size.x), int(preview_size.y), false, Image.FORMAT_RGBA8) | ||
AThousandShips marked this conversation as resolved.
Show resolved
Hide resolved
|
||
white_image.fill(Color.WHITE) | ||
camera_preview.texture = ImageTexture.create_from_image(white_image) | ||
|
||
var rot := camera_feed.feed_transform.get_rotation() | ||
AThousandShips marked this conversation as resolved.
Show resolved
Hide resolved
|
||
var degree := roundi(rad_to_deg(rot)) | ||
camera_preview.rotation = rot | ||
camera_preview.custom_minimum_size.y = camera_display.size.y | ||
|
||
if degree % 180 == 0: | ||
AThousandShips marked this conversation as resolved.
Show resolved
Hide resolved
|
||
camera_display.ratio = preview_size.x / preview_size.y | ||
else: | ||
camera_display.ratio = preview_size.y / preview_size.x | ||
|
||
start_or_stop_button.text = "Stop" | ||
AThousandShips marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
func _on_start_or_stop_button_pressed(change_label: bool = true) -> void: | ||
if camera_feed and camera_feed.feed_is_active: | ||
camera_feed.feed_is_active = false | ||
camera_preview.texture = null | ||
camera_preview.rotation = 0 | ||
if change_label: | ||
start_or_stop_button.text = "Start" | ||
else: | ||
_start_camera_feed() | ||
if change_label: | ||
start_or_stop_button.text = "Stop" | ||
|
||
|
||
func _on_reload_button_pressed() -> void: | ||
_on_start_or_stop_button_pressed(false) | ||
_reload_camera_list() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
uid://dxaoavn781kxe |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.