Skip to content

Commit 17843ed

Browse files
committed
Add a camera feed demo in misc
1 parent 89b2660 commit 17843ed

File tree

10 files changed

+502
-0
lines changed

10 files changed

+502
-0
lines changed

misc/camera_feed/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Camera Feed Demo
2+
3+
A demo that shows how to display live camera feeds from various sources
4+
using Godot's [CameraFeed](https://docs.godotengine.org/en/stable/classes/class_camerafeed.html)
5+
and [CameraServer](https://docs.godotengine.org/en/stable/classes/class_cameraserver.html) APIs.
6+
Supports multiple platforms including desktop, mobile, and web browsers.
7+
8+
Language: GDScript
9+
10+
Renderer: Compatibility, Mobile, Forward+
11+
12+
> Note: this demo requires Godot 4.5 or later
13+
14+
# How does it work?
15+
16+
The demo uses `CameraServer` to enumerate available camera devices and display their feeds in real-time. Key features include:
17+
18+
1. **Camera Detection**: Automatically detects all available camera feeds using `CameraServer.feeds()`.
19+
20+
2. **Platform Support**:
21+
- Handles camera permissions on mobile platforms (Android/iOS)
22+
- Supports web browsers with special monitoring setup
23+
- Works on desktop platforms with standard camera APIs
24+
25+
3. **Feed Formats**:
26+
- RGB format for standard color feeds
27+
- YCbCr format with shader-based conversion for certain devices
28+
- Dynamic format selection based on camera capabilities
29+
30+
4. **Real-time Display**:
31+
- Uses `CameraTexture` to display live camera feeds
32+
- Handles camera rotation and orientation transforms
33+
- Maintains proper aspect ratio for different camera resolutions
34+
35+
5. **Shader Processing**:
36+
- Custom shader (`ycbcr_to_rgb.gdshader`) converts YCbCr feeds to RGB
37+
- Uses BT.709 color space conversion standard for HDTV
38+
39+
The UI provides controls to select cameras, choose formats, and start/stop the feed display.
40+
41+
## Screenshots
42+
43+
![Screenshot](screenshots/camera_feed.png)

misc/camera_feed/camerafeed.gd

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
extends Control
2+
3+
@onready var camera_display := $CameraDisplay
4+
@onready var camera_preview := $CameraDisplay/CameraPreview
5+
@onready var camera_list := $DrawerContainer/Drawer/DrawerContent/VBoxContainer/CameraList
6+
@onready var format_list := $DrawerContainer/Drawer/DrawerContent/VBoxContainer/FormatList
7+
@onready var start_or_stop_button := $DrawerContainer/Drawer/DrawerContent/VBoxContainer/ButtonContainer/StartOrStopButton
8+
@onready var reload_button := $DrawerContainer/Drawer/DrawerContent/VBoxContainer/ButtonContainer/ReloadButton
9+
10+
var camera_feed: CameraFeed
11+
12+
const defaultWebResolution: Dictionary = {
13+
"width": 640,
14+
"height": 480
15+
}
16+
17+
func _ready() -> void:
18+
_adjust_ui()
19+
_reload_camera_list()
20+
21+
func _adjust_ui() -> void:
22+
camera_display.size = camera_display.get_parent_area_size() - Vector2.ONE * 40
23+
camera_preview.custom_minimum_size = camera_display.size
24+
camera_preview.position = camera_display.size / 2
25+
26+
func _reload_camera_list() -> void:
27+
camera_list.clear()
28+
format_list.clear()
29+
30+
var os_name := OS.get_name()
31+
# Request camera permission on mobile
32+
if os_name in ["Android", "iOS"]:
33+
var permissions = OS.get_granted_permissions()
34+
if not "CAMERA" in permissions:
35+
if not OS.request_permission("CAMERA"):
36+
print("CAMERA permission not granted")
37+
return
38+
39+
if CameraServer.monitoring_feeds:
40+
CameraServer.monitoring_feeds = false
41+
await get_tree().process_frame
42+
CameraServer.monitoring_feeds = true
43+
await get_tree().process_frame
44+
# Wait for monitoring to be ready on web platform
45+
if os_name == "Web":
46+
while not CameraServer.monitoring_feeds:
47+
await get_tree().process_frame
48+
49+
func _on_feeds_update() -> void:
50+
# Get available camera feeds
51+
var feeds = CameraServer.feeds()
52+
if feeds.is_empty():
53+
camera_list.add_item("No cameras found")
54+
camera_list.disabled = true
55+
format_list.add_item("No formats available")
56+
format_list.disabled = true
57+
start_or_stop_button.disabled = true
58+
return
59+
60+
camera_list.disabled = false
61+
for i in feeds.size():
62+
var feed: CameraFeed = feeds[i]
63+
camera_list.add_item(feed.get_name())
64+
65+
# Auto-select first camera
66+
camera_list.selected = 0
67+
_on_camera_list_item_selected(0)
68+
69+
func _on_camera_list_item_selected(index: int) -> void:
70+
var camera_feeds := CameraServer.feeds()
71+
if index < 0 or index >= camera_feeds.size():
72+
return
73+
74+
# Stop previous camera if active
75+
if camera_feed and camera_feed.feed_is_active:
76+
camera_feed.feed_is_active = false
77+
78+
# Get selected camera feed
79+
camera_feed = camera_feeds[index]
80+
81+
# Update format list
82+
_update_format_list()
83+
84+
func _update_format_list() -> void:
85+
format_list.clear()
86+
87+
if not camera_feed:
88+
return
89+
90+
var formats = camera_feed.get_formats()
91+
if formats.is_empty():
92+
format_list.add_item("No formats available")
93+
format_list.disabled = true
94+
start_or_stop_button.disabled = true
95+
return
96+
97+
format_list.disabled = false
98+
for format in formats:
99+
var resolution := str(format["width"]) + "x" + str(format["height"])
100+
var item := "%s - %s" % [format["format"], resolution]
101+
if OS.get_name() == "Windows":
102+
item += " : %s / %s" % [format["frame_denominator"], format["frame_numerator"]]
103+
format_list.add_item(item)
104+
105+
# Auto-select first format
106+
format_list.selected = 0
107+
_on_format_list_item_selected(0)
108+
109+
func _on_format_list_item_selected(index: int) -> void:
110+
if not camera_feed:
111+
return
112+
113+
var formats := camera_feed.get_formats()
114+
if index < 0 or index >= formats.size():
115+
return
116+
var os_name := OS.get_name()
117+
var parameters: Dictionary = defaultWebResolution if os_name == "Web" else {}
118+
camera_feed.set_format(index, parameters)
119+
_start_camera_feed()
120+
121+
func _start_camera_feed() -> void:
122+
if not camera_feed:
123+
return
124+
125+
if not camera_feed.frame_changed.is_connected(_on_frame_changed):
126+
camera_feed.frame_changed.connect(_on_frame_changed, ConnectFlags.CONNECT_ONE_SHOT | ConnectFlags.CONNECT_DEFERRED)
127+
# Start the feed
128+
camera_feed.feed_is_active = true
129+
130+
func _on_frame_changed() -> void:
131+
var datatype := camera_feed.get_datatype() as CameraFeed.FeedDataType
132+
var preview_size := Vector2.ZERO
133+
var mat: ShaderMaterial = camera_preview.material
134+
var rgb_texture: CameraTexture = mat.get_shader_parameter("rgb_texture")
135+
var y_texture: CameraTexture = mat.get_shader_parameter("y_texture")
136+
var cbcr_texture: CameraTexture = mat.get_shader_parameter("cbcr_texture")
137+
rgb_texture.which_feed = CameraServer.FeedImage.FEED_RGBA_IMAGE
138+
y_texture.which_feed = CameraServer.FeedImage.FEED_Y_IMAGE
139+
cbcr_texture.which_feed = CameraServer.FeedImage.FEED_CBCR_IMAGE
140+
match datatype:
141+
CameraFeed.FeedDataType.FEED_RGB:
142+
rgb_texture.camera_feed_id = camera_feed.get_id()
143+
mat.set_shader_parameter("rgb_texture", rgb_texture)
144+
mat.set_shader_parameter("mode", 0)
145+
preview_size = rgb_texture.get_size()
146+
CameraFeed.FeedDataType.FEED_YCBCR_SEP:
147+
y_texture.camera_feed_id = camera_feed.get_id()
148+
cbcr_texture.camera_feed_id = camera_feed.get_id()
149+
mat.set_shader_parameter("y_texture", y_texture)
150+
mat.set_shader_parameter("cbcr_texture", cbcr_texture)
151+
mat.set_shader_parameter("mode", 1)
152+
preview_size = y_texture.get_size()
153+
_:
154+
print("YCbCr format not fully implemented yet")
155+
return
156+
var white_image := Image.create(int(preview_size.x), int(preview_size.y), false, Image.FORMAT_RGBA8)
157+
white_image.fill(Color.WHITE)
158+
camera_preview.texture = ImageTexture.create_from_image(white_image)
159+
var rot := camera_feed.feed_transform.get_rotation()
160+
var degree := roundi(rad_to_deg(rot))
161+
camera_preview.rotation = rot
162+
camera_preview.custom_minimum_size.y = camera_display.size.y
163+
if degree % 180 == 0:
164+
camera_display.ratio = preview_size.x / preview_size.y
165+
else:
166+
camera_display.ratio = preview_size.y / preview_size.x
167+
start_or_stop_button.text = "Stop"
168+
169+
func _on_start_or_stop_button_pressed(change_label: bool = true) -> void:
170+
if camera_feed and camera_feed.feed_is_active:
171+
camera_feed.feed_is_active = false
172+
camera_preview.texture = null
173+
camera_preview.rotation = 0
174+
if change_label:
175+
start_or_stop_button.text = "Start"
176+
else:
177+
_start_camera_feed()
178+
if change_label:
179+
start_or_stop_button.text = "Stop"
180+
181+
func _on_reload_button_pressed() -> void:
182+
_on_start_or_stop_button_pressed(false)
183+
_reload_camera_list()

misc/camera_feed/camerafeed.gd.uid

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://dxaoavn781kxe

misc/camera_feed/camerafeed.tscn

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
[gd_scene load_steps=10 format=3 uid="uid://oiv4p8ii3am4"]
2+
3+
[ext_resource type="Script" uid="uid://dxaoavn781kxe" path="res://camerafeed.gd" id="1_fuswq"]
4+
[ext_resource type="Shader" uid="uid://dhjh7s6i7jnlp" path="res://ycbcr_to_rgb.gdshader" id="2_0uyi5"]
5+
6+
[sub_resource type="CameraTexture" id="CameraTexture_7c2aw"]
7+
8+
[sub_resource type="CameraTexture" id="CameraTexture_nyeft"]
9+
10+
[sub_resource type="CameraTexture" id="CameraTexture_xep8u"]
11+
12+
[sub_resource type="ShaderMaterial" id="ShaderMaterial_lgiw1"]
13+
shader = ExtResource("2_0uyi5")
14+
shader_parameter/rgb_texture = SubResource("CameraTexture_nyeft")
15+
shader_parameter/y_texture = SubResource("CameraTexture_xep8u")
16+
shader_parameter/cbcr_texture = SubResource("CameraTexture_7c2aw")
17+
shader_parameter/mode = 0
18+
19+
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1"]
20+
bg_color = Color(0.15, 0.15, 0.15, 0.95)
21+
corner_radius_top_left = 20
22+
corner_radius_top_right = 20
23+
shadow_color = Color(0, 0, 0, 0.3)
24+
shadow_size = 5
25+
shadow_offset = Vector2(0, -2)
26+
27+
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_2"]
28+
bg_color = Color(0.2, 0.2, 0.2, 1)
29+
corner_radius_top_left = 10
30+
corner_radius_top_right = 10
31+
corner_radius_bottom_right = 10
32+
corner_radius_bottom_left = 10
33+
34+
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_3"]
35+
bg_color = Color(0.3, 0.3, 0.3, 1)
36+
corner_radius_top_left = 10
37+
corner_radius_top_right = 10
38+
corner_radius_bottom_right = 10
39+
corner_radius_bottom_left = 10
40+
41+
[node name="CameraApp" type="Control"]
42+
layout_mode = 3
43+
anchors_preset = 15
44+
anchor_right = 1.0
45+
anchor_bottom = 1.0
46+
grow_horizontal = 2
47+
grow_vertical = 2
48+
script = ExtResource("1_fuswq")
49+
50+
[node name="Background" type="ColorRect" parent="."]
51+
layout_mode = 1
52+
anchors_preset = 15
53+
anchor_right = 1.0
54+
anchor_bottom = 1.0
55+
grow_horizontal = 2
56+
grow_vertical = 2
57+
color = Color(0, 0, 0, 1)
58+
59+
[node name="CameraDisplay" type="AspectRatioContainer" parent="."]
60+
layout_mode = 0
61+
offset_left = 20.0
62+
offset_top = 20.0
63+
offset_right = 700.0
64+
offset_bottom = 1260.0
65+
stretch_mode = 1
66+
67+
[node name="CameraPreview" type="TextureRect" parent="CameraDisplay"]
68+
material = SubResource("ShaderMaterial_lgiw1")
69+
layout_mode = 2
70+
stretch_mode = 5
71+
72+
[node name="DrawerContainer" type="Control" parent="."]
73+
modulate = Color(1, 1, 1, 0.5019608)
74+
layout_mode = 1
75+
anchors_preset = 10
76+
anchor_right = 1.0
77+
offset_top = 160.0
78+
offset_bottom = 160.0
79+
grow_horizontal = 2
80+
81+
[node name="Drawer" type="PanelContainer" parent="DrawerContainer"]
82+
layout_mode = 1
83+
anchors_preset = 15
84+
anchor_right = 1.0
85+
anchor_bottom = 1.0
86+
grow_horizontal = 2
87+
grow_vertical = 2
88+
theme_override_styles/panel = SubResource("StyleBoxFlat_1")
89+
90+
[node name="DrawerContent" type="MarginContainer" parent="DrawerContainer/Drawer"]
91+
layout_mode = 2
92+
theme_override_constants/margin_left = 20
93+
theme_override_constants/margin_top = 20
94+
theme_override_constants/margin_right = 20
95+
theme_override_constants/margin_bottom = 20
96+
97+
[node name="VBoxContainer" type="VBoxContainer" parent="DrawerContainer/Drawer/DrawerContent"]
98+
layout_mode = 2
99+
theme_override_constants/separation = 15
100+
101+
[node name="HandleBar" type="Control" parent="DrawerContainer/Drawer/DrawerContent/VBoxContainer"]
102+
custom_minimum_size = Vector2(0, 20)
103+
layout_mode = 2
104+
105+
[node name="Bar" type="ColorRect" parent="DrawerContainer/Drawer/DrawerContent/VBoxContainer/HandleBar"]
106+
layout_mode = 1
107+
anchors_preset = 8
108+
anchor_left = 0.5
109+
anchor_top = 0.5
110+
anchor_right = 0.5
111+
anchor_bottom = 0.5
112+
offset_left = -30.0
113+
offset_top = -2.0
114+
offset_right = 30.0
115+
offset_bottom = 2.0
116+
grow_horizontal = 2
117+
grow_vertical = 2
118+
color = Color(0.5, 0.5, 0.5, 1)
119+
120+
[node name="ButtonContainer" type="HBoxContainer" parent="DrawerContainer/Drawer/DrawerContent/VBoxContainer"]
121+
layout_mode = 2
122+
theme_override_constants/separation = 10
123+
124+
[node name="StartOrStopButton" type="Button" parent="DrawerContainer/Drawer/DrawerContent/VBoxContainer/ButtonContainer"]
125+
custom_minimum_size = Vector2(0, 50)
126+
layout_mode = 2
127+
size_flags_horizontal = 3
128+
theme_override_styles/normal = SubResource("StyleBoxFlat_2")
129+
theme_override_styles/pressed = SubResource("StyleBoxFlat_2")
130+
theme_override_styles/hover = SubResource("StyleBoxFlat_3")
131+
text = "Stop"
132+
133+
[node name="ReloadButton" type="Button" parent="DrawerContainer/Drawer/DrawerContent/VBoxContainer/ButtonContainer"]
134+
custom_minimum_size = Vector2(0, 50)
135+
layout_mode = 2
136+
size_flags_horizontal = 3
137+
theme_override_styles/normal = SubResource("StyleBoxFlat_2")
138+
theme_override_styles/pressed = SubResource("StyleBoxFlat_2")
139+
theme_override_styles/hover = SubResource("StyleBoxFlat_3")
140+
text = "Reload"
141+
142+
[node name="CameraLabel" type="Label" parent="DrawerContainer/Drawer/DrawerContent/VBoxContainer"]
143+
layout_mode = 2
144+
text = "Camera"
145+
146+
[node name="CameraList" type="OptionButton" parent="DrawerContainer/Drawer/DrawerContent/VBoxContainer"]
147+
custom_minimum_size = Vector2(0, 40)
148+
layout_mode = 2
149+
150+
[node name="FormatLabel" type="Label" parent="DrawerContainer/Drawer/DrawerContent/VBoxContainer"]
151+
layout_mode = 2
152+
text = "Format"
153+
154+
[node name="FormatList" type="OptionButton" parent="DrawerContainer/Drawer/DrawerContent/VBoxContainer"]
155+
custom_minimum_size = Vector2(0, 40)
156+
layout_mode = 2
157+
158+
[connection signal="pressed" from="DrawerContainer/Drawer/DrawerContent/VBoxContainer/ButtonContainer/StartOrStopButton" to="." method="_on_start_or_stop_button_pressed"]
159+
[connection signal="pressed" from="DrawerContainer/Drawer/DrawerContent/VBoxContainer/ButtonContainer/ReloadButton" to="." method="_on_reload_button_pressed"]
160+
[connection signal="item_selected" from="DrawerContainer/Drawer/DrawerContent/VBoxContainer/CameraList" to="." method="_on_camera_list_item_selected"]
161+
[connection signal="item_selected" from="DrawerContainer/Drawer/DrawerContent/VBoxContainer/FormatList" to="." method="_on_format_list_item_selected"]

misc/camera_feed/icon.svg

Lines changed: 1 addition & 0 deletions
Loading

0 commit comments

Comments
 (0)