Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
43 changes: 43 additions & 0 deletions misc/camera_feed/README.md
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

![Screenshot](screenshots/camera_feed.png)
187 changes: 187 additions & 0 deletions misc/camera_feed/camerafeed.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
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
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"height": 480
"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
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# Request camera permission on mobile
# Request camera permission on mobile.

Per the comment style, for all

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:
CameraServer.monitoring_feeds = false
await get_tree().process_frame
CameraServer.monitoring_feeds = true

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 | ConnectFlags.CONNECT_DEFERRED)
# 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
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
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:
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("YCbCr format not fully implemented yet")
return
var white_image := Image.create(int(preview_size.x), int(preview_size.y), false, Image.FORMAT_RGBA8)
white_image.fill(Color.WHITE)
camera_preview.texture = ImageTexture.create_from_image(white_image)
var rot := camera_feed.feed_transform.get_rotation()
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:
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"

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()
1 change: 1 addition & 0 deletions misc/camera_feed/camerafeed.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://dxaoavn781kxe
164 changes: 164 additions & 0 deletions misc/camera_feed/camerafeed.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
[gd_scene load_steps=11 format=3 uid="uid://oiv4p8ii3am4"]

[ext_resource type="Script" uid="uid://dxaoavn781kxe" path="res://camerafeed.gd" id="1_fuswq"]
[ext_resource type="Shader" uid="uid://dhjh7s6i7jnlp" path="res://ycbcr_to_rgb.gdshader" id="2_0uyi5"]

[sub_resource type="CameraTexture" id="CameraTexture_7c2aw"]

[sub_resource type="CameraTexture" id="CameraTexture_nyeft"]

[sub_resource type="CameraTexture" id="CameraTexture_xep8u"]

[sub_resource type="CameraTexture" id="CameraTexture_fuswq"]

[sub_resource type="ShaderMaterial" id="ShaderMaterial_lgiw1"]
shader = ExtResource("2_0uyi5")
shader_parameter/rgb_texture = SubResource("CameraTexture_nyeft")
shader_parameter/y_texture = SubResource("CameraTexture_xep8u")
shader_parameter/cbcr_texture = SubResource("CameraTexture_7c2aw")
shader_parameter/ycbcr_texture = SubResource("CameraTexture_fuswq")
shader_parameter/mode = 0

[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1"]
bg_color = Color(0.15, 0.15, 0.15, 0.95)
corner_radius_top_left = 20
corner_radius_top_right = 20
shadow_color = Color(0, 0, 0, 0.3)
shadow_size = 5
shadow_offset = Vector2(0, -2)

[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_2"]
bg_color = Color(0.2, 0.2, 0.2, 1)
corner_radius_top_left = 10
corner_radius_top_right = 10
corner_radius_bottom_right = 10
corner_radius_bottom_left = 10

[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_3"]
bg_color = Color(0.3, 0.3, 0.3, 1)
corner_radius_top_left = 10
corner_radius_top_right = 10
corner_radius_bottom_right = 10
corner_radius_bottom_left = 10

[node name="CameraApp" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_fuswq")

[node name="Background" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0, 0, 1)

[node name="CameraDisplay" type="AspectRatioContainer" parent="."]
layout_mode = 0
offset_left = 20.0
offset_top = 20.0
offset_right = 700.0
offset_bottom = 1260.0
stretch_mode = 1

[node name="CameraPreview" type="TextureRect" parent="CameraDisplay"]
material = SubResource("ShaderMaterial_lgiw1")
layout_mode = 2
stretch_mode = 5

[node name="DrawerContainer" type="Control" parent="."]
modulate = Color(1, 1, 1, 0.5019608)
layout_mode = 1
anchors_preset = 10
anchor_right = 1.0
offset_top = 160.0
offset_bottom = 160.0
grow_horizontal = 2

[node name="Drawer" type="PanelContainer" parent="DrawerContainer"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_1")

[node name="DrawerContent" type="MarginContainer" parent="DrawerContainer/Drawer"]
layout_mode = 2
theme_override_constants/margin_left = 20
theme_override_constants/margin_top = 20
theme_override_constants/margin_right = 20
theme_override_constants/margin_bottom = 20

[node name="VBoxContainer" type="VBoxContainer" parent="DrawerContainer/Drawer/DrawerContent"]
layout_mode = 2
theme_override_constants/separation = 15

[node name="HandleBar" type="Control" parent="DrawerContainer/Drawer/DrawerContent/VBoxContainer"]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2

[node name="Bar" type="ColorRect" parent="DrawerContainer/Drawer/DrawerContent/VBoxContainer/HandleBar"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -30.0
offset_top = -2.0
offset_right = 30.0
offset_bottom = 2.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.5, 0.5, 0.5, 1)

[node name="ButtonContainer" type="HBoxContainer" parent="DrawerContainer/Drawer/DrawerContent/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 10

[node name="StartOrStopButton" type="Button" parent="DrawerContainer/Drawer/DrawerContent/VBoxContainer/ButtonContainer"]
custom_minimum_size = Vector2(0, 50)
layout_mode = 2
size_flags_horizontal = 3
theme_override_styles/normal = SubResource("StyleBoxFlat_2")
theme_override_styles/pressed = SubResource("StyleBoxFlat_2")
theme_override_styles/hover = SubResource("StyleBoxFlat_3")
text = "Stop"

[node name="ReloadButton" type="Button" parent="DrawerContainer/Drawer/DrawerContent/VBoxContainer/ButtonContainer"]
custom_minimum_size = Vector2(0, 50)
layout_mode = 2
size_flags_horizontal = 3
theme_override_styles/normal = SubResource("StyleBoxFlat_2")
theme_override_styles/pressed = SubResource("StyleBoxFlat_2")
theme_override_styles/hover = SubResource("StyleBoxFlat_3")
text = "Reload"

[node name="CameraLabel" type="Label" parent="DrawerContainer/Drawer/DrawerContent/VBoxContainer"]
layout_mode = 2
text = "Camera"

[node name="CameraList" type="OptionButton" parent="DrawerContainer/Drawer/DrawerContent/VBoxContainer"]
custom_minimum_size = Vector2(0, 40)
layout_mode = 2

[node name="FormatLabel" type="Label" parent="DrawerContainer/Drawer/DrawerContent/VBoxContainer"]
layout_mode = 2
text = "Format"

[node name="FormatList" type="OptionButton" parent="DrawerContainer/Drawer/DrawerContent/VBoxContainer"]
custom_minimum_size = Vector2(0, 40)
layout_mode = 2

[connection signal="pressed" from="DrawerContainer/Drawer/DrawerContent/VBoxContainer/ButtonContainer/StartOrStopButton" to="." method="_on_start_or_stop_button_pressed"]
[connection signal="pressed" from="DrawerContainer/Drawer/DrawerContent/VBoxContainer/ButtonContainer/ReloadButton" to="." method="_on_reload_button_pressed"]
[connection signal="item_selected" from="DrawerContainer/Drawer/DrawerContent/VBoxContainer/CameraList" to="." method="_on_camera_list_item_selected"]
[connection signal="item_selected" from="DrawerContainer/Drawer/DrawerContent/VBoxContainer/FormatList" to="." method="_on_format_list_item_selected"]
Loading