Skip to content

Commit 115a5c2

Browse files
committed
Add capability to stream youtube videos
1 parent 3a45055 commit 115a5c2

File tree

7 files changed

+247
-114
lines changed

7 files changed

+247
-114
lines changed

.github/workflows/main.yml

Lines changed: 77 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -8,82 +8,82 @@ permissions:
88
contents: write # Required to upload release assets
99

1010
jobs:
11-
# build_and_upload_image:
12-
# runs-on: ubuntu-22.04-arm
13-
# steps:
14-
# - name: "Checkout code"
15-
# uses: actions/checkout@v4
16-
17-
# - name: "Set up Python"
18-
# uses: actions/setup-python@v5
19-
# with:
20-
# python-version: '3.10'
21-
22-
# - name: "Set up Node.js"
23-
# uses: actions/setup-node@v3
24-
# with:
25-
# node-version: '22'
26-
# - name: "Install Dependencies"
27-
# run: |
28-
# echo "Installing dependencies..."
29-
# ./install_requirements.sh
30-
31-
# - name: "Run Create Release Script"
32-
# run: |
33-
# echo "Creating release..."
34-
# chmod +x ./create_release.sh
35-
# pip install requests
36-
# # Pass the release name to the script
37-
# ./create_release.sh << ${{ github.event.release.name }}
38-
# - name: "Upload release"
39-
# uses: actions/upload-release-asset@v1
40-
# with:
41-
# upload_url: ${{ github.event.release.upload_url }}
42-
# asset_path: release.tar.gz
43-
# asset_name: release.tar.gz
44-
# asset_content_type: application/gzip
45-
# env:
46-
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47-
48-
# - name: "Checkout Pi Image Repository"
49-
# uses: actions/checkout@v4
50-
# with:
51-
# repository: DeepWaterExploration/pi-gen
52-
# path: pi-gen
53-
# - name: "Install Dependencies for Pi Image Build"
54-
# run: |
55-
# echo "Installing dependencies..."
56-
# sudo apt-get install coreutils quilt parted qemu-user-static debootstrap zerofree zip \
57-
# dosfstools libarchive-tools libcap2-bin grep rsync xz-utils file git curl bc \
58-
# gpg pigz xxd arch-test
59-
60-
# - name: "Build Pi Image"
61-
# run: |
62-
# echo "Building Pi Image..."
63-
# cd pi-gen
64-
# echo "" >> ./config
65-
# echo "export DWE_VERSION=\"${{ github.event.release.name }}\"" >> ./config
66-
# echo "" >> ./config
67-
# sudo ./build.sh -c ./config
68-
69-
# - name: "Fix Permissions of Deploy Directory"
70-
# run: sudo chown -R runner:runner pi-gen/deploy
71-
72-
# - name: "Find Pi Image File"
73-
# id: find_image
74-
# run: |
75-
# IMAGE_FILE=$(find pi-gen/deploy -name "*.zip" | head -1)
76-
# echo "image_file=$IMAGE_FILE" >> $GITHUB_OUTPUT
77-
78-
# - name: "Upload Pi Image to Release"
79-
# uses: actions/upload-release-asset@v1
80-
# with:
81-
# upload_url: ${{ github.event.release.upload_url }}
82-
# asset_path: ${{ steps.find_image.outputs.image_file }}
83-
# asset_name: DWE_OS_${{ github.event.release.name }}.zip
84-
# asset_content_type: application/zip
85-
# env:
86-
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
11+
build_and_upload_image:
12+
runs-on: ubuntu-22.04-arm
13+
steps:
14+
- name: "Checkout code"
15+
uses: actions/checkout@v4
16+
17+
- name: "Set up Python"
18+
uses: actions/setup-python@v5
19+
with:
20+
python-version: '3.10'
21+
22+
- name: "Set up Node.js"
23+
uses: actions/setup-node@v3
24+
with:
25+
node-version: '22'
26+
- name: "Install Dependencies"
27+
run: |
28+
echo "Installing dependencies..."
29+
./install_requirements.sh
30+
31+
- name: "Run Create Release Script"
32+
run: |
33+
echo "Creating release..."
34+
chmod +x ./create_release.sh
35+
pip install requests
36+
# Pass the release name to the script
37+
./create_release.sh << ${{ github.event.release.name }}
38+
- name: "Upload release"
39+
uses: actions/upload-release-asset@v1
40+
with:
41+
upload_url: ${{ github.event.release.upload_url }}
42+
asset_path: release.tar.gz
43+
asset_name: release.tar.gz
44+
asset_content_type: application/gzip
45+
env:
46+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47+
48+
- name: "Checkout Pi Image Repository"
49+
uses: actions/checkout@v4
50+
with:
51+
repository: DeepWaterExploration/pi-gen
52+
path: pi-gen
53+
- name: "Install Dependencies for Pi Image Build"
54+
run: |
55+
echo "Installing dependencies..."
56+
sudo apt-get install coreutils quilt parted qemu-user-static debootstrap zerofree zip \
57+
dosfstools libarchive-tools libcap2-bin grep rsync xz-utils file git curl bc \
58+
gpg pigz xxd arch-test
59+
60+
- name: "Build Pi Image"
61+
run: |
62+
echo "Building Pi Image..."
63+
cd pi-gen
64+
echo "" >> ./config
65+
echo "export DWE_VERSION=\"${{ github.event.release.name }}\"" >> ./config
66+
echo "" >> ./config
67+
sudo ./build.sh -c ./config
68+
69+
- name: "Fix Permissions of Deploy Directory"
70+
run: sudo chown -R runner:runner pi-gen/deploy
71+
72+
- name: "Find Pi Image File"
73+
id: find_image
74+
run: |
75+
IMAGE_FILE=$(find pi-gen/deploy -name "*.zip" | head -1)
76+
echo "image_file=$IMAGE_FILE" >> $GITHUB_OUTPUT
77+
78+
- name: "Upload Pi Image to Release"
79+
uses: actions/upload-release-asset@v1
80+
with:
81+
upload_url: ${{ github.event.release.upload_url }}
82+
asset_path: ${{ steps.find_image.outputs.image_file }}
83+
asset_name: DWE_OS_${{ github.event.release.name }}.zip
84+
asset_content_type: application/zip
85+
env:
86+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
8787

8888
build_docker_image:
8989
runs-on: ubuntu-22.04-arm
@@ -110,11 +110,4 @@ jobs:
110110
- name: "Build Docker Image"
111111
run: |
112112
echo "Building Docker Image..."
113-
sudo docker buildx build --platform linux/arm/v7 -t deepwaterexploration/dwe_os_2 --load -f docker/Dockerfile .
114-
docker tag deepwaterexploration/dwe_os_2 deepwaterexploration/dwe_os_2:${{ github.event.release.name }}
115-
116-
- name: "Push Docker Image"
117-
run: |
118-
echo "Pushing Docker Image..."
119-
docker push deepwaterexploration/dwe_os_2:${{ github.event.release.name }}
120-
docker push deepwaterexploration/dwe_os_2:latest
113+
docker buildx build --platform linux/arm/v7 -t deepwaterexploration/dwe_os_2:${{ github.event.release.name }} --push -f docker/Dockerfile .

backend_py/src/routes/cameras.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@ def get_devices(request: Request) -> List[DeviceModel]:
2121
@camera_router.post('/devices/configure_stream', summary='Configure a stream')
2222
async def configure_stream(request: Request, stream_info: StreamInfoModel):
2323
device_manager: DeviceManager = request.app.state.device_manager
24-
2524
device_manager.configure_device_stream(stream_info)
26-
25+
print(f"{stream_info.stream_type} stream configured for device: {stream_info.bus_info}")
2726
for device in device_manager.devices:
2827
if device.bus_info == stream_info.bus_info:
2928
if device.device_type != DeviceType.STELLARHD_FOLLOWER:

backend_py/src/services/cameras/device_manager.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def configure_device_stream(self, stream_info: StreamInfoModel) -> bool:
130130
"""
131131
Configure a device's stream with the given stream info
132132
"""
133+
133134
device = self._find_device_with_bus_info(stream_info.bus_info)
134135

135136
stream_format = stream_info.stream_format
@@ -138,9 +139,10 @@ def configure_device_stream(self, stream_info: StreamInfoModel) -> bool:
138139
interval = stream_format.interval
139140
encode_type: StreamEncodeTypeEnum = stream_info.encode_type
140141
endpoints = stream_info.endpoints
142+
stream_type: StreamTypeEnum = stream_info.stream_type
141143

142144
device.configure_stream(
143-
encode_type, width, height, interval, StreamTypeEnum.UDP, endpoints
145+
encode_type, width, height, interval, stream_type, endpoints
144146
)
145147

146148
if stream_info.enabled:

backend_py/src/services/cameras/pydantic_schemas.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class StreamEncodeTypeEnum(str, Enum):
3535

3636
class StreamTypeEnum(str, Enum):
3737
UDP = "UDP"
38+
RTMP = "RTMP"
3839

3940

4041
class H264Mode(IntEnum):
@@ -134,6 +135,8 @@ class Config:
134135
class StreamEndpointModel(BaseModel):
135136
host: str
136137
port: int
138+
# For RTMP streams, this will be the full RTMP URL
139+
rtmp_url: Optional[str] = None
137140

138141
class Config:
139142
from_attributes = True
@@ -201,6 +204,7 @@ class StreamInfoModel(BaseModel):
201204
encode_type: StreamEncodeTypeEnum
202205
enabled: bool
203206
endpoints: List[StreamEndpointModel]
207+
stream_type: Optional[StreamTypeEnum] = StreamTypeEnum.UDP
204208

205209
class Config:
206210
from_attributes = True

backend_py/src/services/cameras/stream.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,14 @@ class Stream(events.EventEmitter):
2525
software_h264_bitrate = 5000
2626

2727
def _construct_pipeline(self):
28-
return f"{self._build_source()} ! {self._construct_caps()} ! {self._build_payload()} ! {self._build_sink()}"
28+
video_pipeline = f"{self._build_source()} ! {self._construct_caps()} ! {self._build_payload()}"
29+
30+
if self.stream_type == StreamTypeEnum.RTMP:
31+
# Add silent audio source for RTMP streams
32+
audio_pipeline = "audiotestsrc wave=silence ! audioconvert ! voaacenc ! queue"
33+
return f"{video_pipeline} ! mux. {audio_pipeline} ! mux. flvmux name=mux ! rtmpsink location={self.endpoints[0].rtmp_url if self.endpoints else ''}"
34+
else:
35+
return f"{video_pipeline} ! {self._build_sink()}"
2936

3037
def _get_format(self):
3138
match self.encode_type:
@@ -47,11 +54,20 @@ def _construct_caps(self):
4754
def _build_payload(self):
4855
match self.encode_type:
4956
case StreamEncodeTypeEnum.H264:
50-
return "h264parse ! queue ! rtph264pay config-interval=10 pt=96"
57+
if self.stream_type == StreamTypeEnum.RTMP:
58+
return "h264parse ! queue"
59+
else:
60+
return "h264parse ! queue ! rtph264pay config-interval=10 pt=96"
5161
case StreamEncodeTypeEnum.MJPG:
52-
return "rtpjpegpay"
62+
if self.stream_type == StreamTypeEnum.RTMP:
63+
return "queue" # MJPEG can't be used directly with RTMP
64+
else:
65+
return "rtpjpegpay"
5366
case StreamEncodeTypeEnum.SOFTWARE_H264:
54-
return f"jpegdec ! queue ! x264enc byte-stream=true tune=zerolatency bitrate={self.software_h264_bitrate} speed-preset=ultrafast ! rtph264pay config-interval=10 pt=96"
67+
if self.stream_type == StreamTypeEnum.RTMP:
68+
return f"jpegdec ! queue ! x264enc byte-stream=true tune=zerolatency bitrate={self.software_h264_bitrate} speed-preset=ultrafast"
69+
else:
70+
return f"jpegdec ! queue ! x264enc byte-stream=true tune=zerolatency bitrate={self.software_h264_bitrate} speed-preset=ultrafast ! rtph264pay config-interval=10 pt=96"
5571
case _:
5672
return ""
5773

@@ -67,6 +83,14 @@ def _build_sink(self):
6783
sink += ","
6884

6985
return sink
86+
case StreamTypeEnum.RTMP:
87+
if len(self.endpoints) == 0 or not self.endpoints[0].rtmp_url:
88+
return "fakesink"
89+
# For RTMP streaming without audio (handled in _construct_pipeline when audio is enabled)
90+
if not self.include_audio:
91+
return f"flvmux ! rtmpsink location={self.endpoints[0].rtmp_url}"
92+
else:
93+
return "" # Handled in _construct_pipeline
7094
case _:
7195
return ""
7296

@@ -117,6 +141,9 @@ def stop(self):
117141
def _run_pipeline(self):
118142
pipeline_str = self._construct_pipeline()
119143
self.logger.info(pipeline_str)
144+
145+
# Use shell=True to handle complex GStreamer pipelines properly
146+
# This is safer for GStreamer commands with complex arguments
120147
self._process = subprocess.Popen(
121148
f"gst-launch-1.0 {pipeline_str}".split(" "),
122149
stdout=subprocess.DEVNULL,

0 commit comments

Comments
 (0)