Skip to content

Commit 31daada

Browse files
committed
Add a camera feed demo in misc
1 parent 9acb552 commit 31daada

File tree

10 files changed

+486
-0
lines changed

10 files changed

+486
-0
lines changed

misc/camerafeed/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
# How does it work?
13+
14+
The demo uses `CameraServer` to enumerate available camera devices and display their feeds in real-time. Key features include:
15+
16+
1. **Camera Detection**: Automatically detects all available camera feeds using `CameraServer.feeds()`.
17+
18+
2. **Platform Support**:
19+
- Handles camera permissions on mobile platforms (Android/iOS)
20+
- Supports web browsers with special monitoring setup
21+
- Works on desktop platforms with standard camera APIs
22+
23+
3. **Feed Formats**:
24+
- RGB format for standard color feeds
25+
- YCbCr format with shader-based conversion for certain devices
26+
- Dynamic format selection based on camera capabilities
27+
28+
4. **Real-time Display**:
29+
- Uses `CameraTexture` to display live camera feeds
30+
- Handles camera rotation and orientation transforms
31+
- Maintains proper aspect ratio for different camera resolutions
32+
33+
5. **Shader Processing**:
34+
- Custom shader (`ycbcr_to_rgb.gdshader`) converts YCbCr feeds to RGB
35+
- Uses BT.709 color space conversion standard for HDTV
36+
37+
The UI provides controls to select cameras, choose formats, and start/stop the feed display.
38+
39+
## Screenshots
40+
41+
![Screenshot](screenshots/camera_feed.png)

misc/camerafeed/camerafeed.gd

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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+
CameraServer.monitoring_feeds = true
40+
41+
# Wait for monitoring to be ready on web platform
42+
if os_name == "Web":
43+
while not CameraServer.monitoring_feeds:
44+
await get_tree().process_frame
45+
46+
# Get available camera feeds
47+
var feeds = CameraServer.feeds()
48+
if feeds.is_empty():
49+
camera_list.add_item("No cameras found")
50+
camera_list.disabled = true
51+
format_list.add_item("No formats available")
52+
format_list.disabled = true
53+
start_or_stop_button.disabled = true
54+
return
55+
56+
camera_list.disabled = false
57+
for i in feeds.size():
58+
var feed: CameraFeed = feeds[i]
59+
camera_list.add_item(feed.get_name())
60+
61+
# Auto-select first camera
62+
camera_list.selected = 0
63+
_on_camera_list_item_selected(0)
64+
65+
func _on_camera_list_item_selected(index: int) -> void:
66+
if index < 0 or index >= CameraServer.feeds().size():
67+
return
68+
69+
# Stop previous camera if active
70+
if camera_feed and camera_feed.feed_is_active:
71+
camera_feed.feed_is_active = false
72+
73+
# Get selected camera feed
74+
camera_feed = CameraServer.feeds()[index]
75+
76+
# Update format list
77+
_update_format_list()
78+
79+
func _update_format_list() -> void:
80+
format_list.clear()
81+
82+
if not camera_feed:
83+
return
84+
85+
var formats = camera_feed.get_formats()
86+
if formats.is_empty():
87+
format_list.add_item("No formats available")
88+
format_list.disabled = true
89+
start_or_stop_button.disabled = true
90+
return
91+
92+
format_list.disabled = false
93+
for format in formats:
94+
var resolution := str(format["width"]) + "x" + str(format["height"])
95+
format_list.add_item(format["format"] + " - " + resolution)
96+
97+
# Auto-select first format
98+
format_list.selected = 0
99+
_on_format_list_item_selected(0)
100+
101+
func _on_format_list_item_selected(index: int) -> void:
102+
if not camera_feed:
103+
return
104+
105+
var formats = camera_feed.get_formats()
106+
if index < 0 or index >= formats.size():
107+
return
108+
var os_name = OS.get_name()
109+
var parameters: Dictionary = defaultWebResolution if os_name == "Web" else {}
110+
camera_feed.set_format(index, parameters)
111+
_start_camera_feed()
112+
113+
func _start_camera_feed() -> void:
114+
if not camera_feed:
115+
return
116+
117+
camera_feed.frame_changed.connect(_on_frame_changed, ConnectFlags.CONNECT_ONE_SHOT | ConnectFlags.CONNECT_DEFERRED)
118+
# Start the feed
119+
camera_feed.feed_is_active = true
120+
121+
func _on_frame_changed() -> void:
122+
var datatype := camera_feed.get_datatype() as CameraFeed.FeedDataType
123+
var preview_size := Vector2.ZERO
124+
var mat: ShaderMaterial = camera_preview.material
125+
var rgb_texture: CameraTexture = mat.get_shader_parameter("rgb_texture")
126+
var y_texture: CameraTexture = mat.get_shader_parameter("y_texture")
127+
var cbcr_texture: CameraTexture = mat.get_shader_parameter("cbcr_texture")
128+
rgb_texture.which_feed = CameraServer.FeedImage.FEED_RGBA_IMAGE
129+
y_texture.which_feed = CameraServer.FeedImage.FEED_Y_IMAGE
130+
cbcr_texture.which_feed = CameraServer.FeedImage.FEED_CBCR_IMAGE
131+
match datatype:
132+
CameraFeed.FeedDataType.FEED_RGB:
133+
rgb_texture.camera_feed_id = camera_feed.get_id()
134+
mat.set_shader_parameter("rgb_texture", rgb_texture)
135+
mat.set_shader_parameter("mode", 0)
136+
preview_size = rgb_texture.get_size()
137+
CameraFeed.FeedDataType.FEED_YCBCR_SEP:
138+
y_texture.camera_feed_id = camera_feed.get_id()
139+
cbcr_texture.camera_feed_id = camera_feed.get_id()
140+
mat.set_shader_parameter("y_texture", y_texture)
141+
mat.set_shader_parameter("cbcr_texture", cbcr_texture)
142+
mat.set_shader_parameter("mode", 1)
143+
preview_size = y_texture.get_size()
144+
_:
145+
print("YCbCr format not fully implemented yet")
146+
return
147+
var white_image := Image.create(int(preview_size.x), int(preview_size.y), false, Image.FORMAT_RGBA8)
148+
white_image.fill(Color.WHITE)
149+
camera_preview.texture = ImageTexture.create_from_image(white_image)
150+
var rot := camera_feed.feed_transform.get_rotation()
151+
var degree := roundi(rad_to_deg(rot))
152+
camera_preview.rotation = rot
153+
camera_preview.custom_minimum_size.y = camera_display.size.y
154+
if degree % 180 == 0:
155+
camera_display.ratio = preview_size.x / preview_size.y
156+
else:
157+
camera_display.ratio = preview_size.y / preview_size.x
158+
start_or_stop_button.text = "Stop"
159+
160+
func _on_start_or_stop_button_pressed(change_label: bool = true) -> void:
161+
if camera_feed and camera_feed.feed_is_active:
162+
camera_feed.feed_is_active = false
163+
camera_preview.texture = null
164+
camera_preview.rotation = 0
165+
if change_label:
166+
start_or_stop_button.text = "Start"
167+
else:
168+
_start_camera_feed()
169+
if change_label:
170+
start_or_stop_button.text = "Stop"
171+
172+
func _on_reload_button_pressed() -> void:
173+
_on_start_or_stop_button_pressed(false)
174+
_reload_camera_list()

misc/camerafeed/camerafeed.gd.uid

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

misc/camerafeed/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/camerafeed/icon.svg

Lines changed: 1 addition & 0 deletions
Loading

0 commit comments

Comments
 (0)