Skip to content

Commit 070d6e1

Browse files
committed
Enable RTSP capture and add CI coverage
1 parent 52fa2c5 commit 070d6e1

6 files changed

Lines changed: 183 additions & 1 deletion

File tree

.github/workflows/macos.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ on:
1313
env:
1414
OPENCV_VERSION: 5.0.0
1515
CACHE_VERSION: "1"
16+
MEDIAMTX_VERSION: 1.19.3
1617

1718
jobs:
1819
# ---------------------------------------------------------------------------
@@ -334,6 +335,51 @@ jobs:
334335
with:
335336
dotnet-version: '10.0.x'
336337

338+
- name: Start local RTSP test server
339+
run: |
340+
version="${MEDIAMTX_VERSION}"
341+
archive_name="mediamtx_v${version}_darwin_arm64.tar.gz"
342+
download_base="https://github.com/bluenviron/mediamtx/releases/download/v${version}"
343+
server_dir="${RUNNER_TEMP}/mediamtx"
344+
archive_path="${RUNNER_TEMP}/${archive_name}"
345+
checksums_path="${RUNNER_TEMP}/mediamtx-checksums.sha256"
346+
347+
curl -fL --retry 5 "${download_base}/${archive_name}" -o "${archive_path}"
348+
curl -fL --retry 5 "${download_base}/checksums.sha256" -o "${checksums_path}"
349+
350+
expected_checksum=$(grep -E "[ *]${archive_name}$" "${checksums_path}" | awk '{print $1}')
351+
if [ -z "${expected_checksum}" ]; then
352+
echo "Checksum not found for ${archive_name}"
353+
exit 1
354+
fi
355+
actual_checksum=$(shasum -a 256 "${archive_path}" | awk '{print $1}')
356+
if [ "${actual_checksum}" != "${expected_checksum}" ]; then
357+
echo "Checksum mismatch for ${archive_name}: expected ${expected_checksum}, got ${actual_checksum}"
358+
exit 1
359+
fi
360+
361+
mkdir -p "${server_dir}"
362+
tar -xzf "${archive_path}" -C "${server_dir}"
363+
"${server_dir}/mediamtx" "${GITHUB_WORKSPACE}/test/rtsp/mediamtx.yml" \
364+
>"${RUNNER_TEMP}/mediamtx.log" 2>&1 &
365+
server_pid=$!
366+
367+
for attempt in $(seq 1 30); do
368+
if ! kill -0 "${server_pid}" 2>/dev/null; then
369+
cat "${RUNNER_TEMP}/mediamtx.log"
370+
exit 1
371+
fi
372+
if grep -q "stream is available" "${RUNNER_TEMP}/mediamtx.log"; then
373+
echo "OPENCVSHARP_TEST_RTSP_URL=rtsp://127.0.0.1:8554/test" >> "${GITHUB_ENV}"
374+
echo "OPENCVSHARP_RTSP_SERVER_PID=${server_pid}" >> "${GITHUB_ENV}"
375+
exit 0
376+
fi
377+
sleep 1
378+
done
379+
380+
cat "${RUNNER_TEMP}/mediamtx.log"
381+
exit 1
382+
337383
- name: Test
338384
run: |
339385
cd ${GITHUB_WORKSPACE}/test/OpenCvSharp.Tests
@@ -350,6 +396,13 @@ jobs:
350396
--logger "trx;LogFileName=test-results.trx" \
351397
--blame-crash --blame-crash-dump-type full < /dev/null
352398
399+
- name: Stop local RTSP test server
400+
if: always()
401+
run: |
402+
if [ -n "${OPENCVSHARP_RTSP_SERVER_PID:-}" ]; then
403+
kill "${OPENCVSHARP_RTSP_SERVER_PID}" 2>/dev/null || true
404+
fi
405+
353406
- name: Test Avalonia extensions
354407
run: |
355408
cd ${GITHUB_WORKSPACE}/test/OpenCvSharp.Tests.Avalonia

.github/workflows/manylinux.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ on:
1010
env:
1111
DEBIAN_FRONTEND: noninteractive
1212
OPENCV_VERSION: 5.0.0
13+
MEDIAMTX_VERSION: 1.19.3
1314

1415
jobs:
1516
# ---------------------------------------------------------------------------
@@ -204,7 +205,27 @@ jobs:
204205
with:
205206
dotnet-version: '10.0.x'
206207

208+
- name: Start local RTSP test server
209+
run: |
210+
docker run --detach --rm \
211+
--name opencvsharp-rtsp \
212+
--publish 8554:8554 \
213+
--volume "${GITHUB_WORKSPACE}/test/rtsp/mediamtx.yml:/mediamtx.yml:ro" \
214+
bluenviron/mediamtx:${MEDIAMTX_VERSION}
215+
216+
for attempt in $(seq 1 30); do
217+
if docker logs opencvsharp-rtsp 2>&1 | grep -q "stream is available"; then
218+
exit 0
219+
fi
220+
sleep 1
221+
done
222+
223+
docker logs opencvsharp-rtsp
224+
exit 1
225+
207226
- name: Test (full)
227+
env:
228+
OPENCVSHARP_TEST_RTSP_URL: rtsp://127.0.0.1:8554/test
208229
run: |
209230
cd ${GITHUB_WORKSPACE}/test/OpenCvSharp.Tests
210231
dotnet build -c Release -f net10.0
@@ -224,6 +245,8 @@ jobs:
224245
# Headless has the same module set as full except highgui, so it runs the same xunit
225246
# suite minus the one test class that calls into highgui (WaitKey/PollKey/...).
226247
- name: Test (headless)
248+
env:
249+
OPENCVSHARP_TEST_RTSP_URL: rtsp://127.0.0.1:8554/test
227250
run: |
228251
cd ${GITHUB_WORKSPACE}/test/OpenCvSharp.Tests
229252
cp ${GITHUB_WORKSPACE}/nuget-headless/libOpenCvSharpExtern-headless.so bin/Release/net10.0/libOpenCvSharpExtern.so
@@ -233,6 +256,10 @@ jobs:
233256
--filter "FullyQualifiedName!~OpenCvSharp.Tests.HighGui.HighGuiTest" \
234257
--logger "trx;LogFileName=test-results-headless.trx" < /dev/null
235258
259+
- name: Stop local RTSP test server
260+
if: always()
261+
run: docker stop opencvsharp-rtsp >/dev/null 2>&1 || true
262+
236263
- name: Smoke test (slim)
237264
run: |
238265
cd ${GITHUB_WORKSPACE}/nuget-slim/

.github/workflows/windows.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ on:
1515

1616
env:
1717
OPENCV_VERSION: "5.0.0"
18+
MEDIAMTX_VERSION: "1.19.3"
1819

1920
jobs:
2021
build:
@@ -174,6 +175,55 @@ jobs:
174175
-D NO_INSTALL_TO_TEST=ON
175176
cmake --build src/build_slim --config Release -j 4
176177
178+
- name: Start local RTSP test server
179+
shell: pwsh
180+
run: |
181+
$ErrorActionPreference = "Stop"
182+
$version = $env:MEDIAMTX_VERSION
183+
$archiveName = "mediamtx_v${version}_windows_amd64.zip"
184+
$downloadBase = "https://github.com/bluenviron/mediamtx/releases/download/v${version}"
185+
$serverDir = Join-Path $env:RUNNER_TEMP "mediamtx"
186+
$archivePath = Join-Path $env:RUNNER_TEMP $archiveName
187+
$checksumsPath = Join-Path $env:RUNNER_TEMP "mediamtx-checksums.sha256"
188+
189+
Invoke-WebRequest "$downloadBase/$archiveName" -OutFile $archivePath
190+
Invoke-WebRequest "$downloadBase/checksums.sha256" -OutFile $checksumsPath
191+
192+
$checksumLine = Get-Content $checksumsPath | Where-Object { $_ -match "[ *]$([regex]::Escape($archiveName))$" }
193+
if (-not $checksumLine) { throw "Checksum not found for $archiveName" }
194+
$expectedChecksum = ($checksumLine -split "\s+")[0].ToLowerInvariant()
195+
$actualChecksum = (Get-FileHash $archivePath -Algorithm SHA256).Hash.ToLowerInvariant()
196+
if ($actualChecksum -ne $expectedChecksum) {
197+
throw "Checksum mismatch for ${archiveName}: expected $expectedChecksum, got $actualChecksum"
198+
}
199+
200+
Expand-Archive $archivePath -DestinationPath $serverDir
201+
$stdoutPath = Join-Path $env:RUNNER_TEMP "mediamtx.stdout.log"
202+
$stderrPath = Join-Path $env:RUNNER_TEMP "mediamtx.stderr.log"
203+
$server = Start-Process `
204+
(Join-Path $serverDir "mediamtx.exe") `
205+
-ArgumentList "${env:GITHUB_WORKSPACE}\test\rtsp\mediamtx.yml" `
206+
-RedirectStandardOutput $stdoutPath `
207+
-RedirectStandardError $stderrPath `
208+
-WindowStyle Hidden `
209+
-PassThru
210+
211+
for ($attempt = 1; $attempt -le 30; $attempt++) {
212+
if ($server.HasExited) {
213+
Get-Content $stdoutPath, $stderrPath -ErrorAction SilentlyContinue
214+
throw "MediaMTX exited with code $($server.ExitCode)"
215+
}
216+
if ((Get-Content $stdoutPath -Raw -ErrorAction SilentlyContinue) -match "stream is available") {
217+
"OPENCVSHARP_TEST_RTSP_URL=rtsp://127.0.0.1:8554/test" | Out-File $env:GITHUB_ENV -Append
218+
"OPENCVSHARP_RTSP_SERVER_PID=$($server.Id)" | Out-File $env:GITHUB_ENV -Append
219+
exit 0
220+
}
221+
Start-Sleep -Seconds 1
222+
}
223+
224+
Get-Content $stdoutPath, $stderrPath -ErrorAction SilentlyContinue
225+
throw "Timed out waiting for MediaMTX"
226+
177227
- name: Test
178228
shell: cmd
179229
# --blame-crash* collects a full memory dump and a Sequence_*.xml (listing tests executed,
@@ -182,6 +232,14 @@ jobs:
182232
# re-run. See "Upload crash dumps on failure" below.
183233
run: dotnet test test\OpenCvSharp.Tests -c Release -f net10.0 --runtime win-x64 --blame-crash --blame-crash-dump-type full
184234

235+
- name: Stop local RTSP test server
236+
if: always()
237+
shell: pwsh
238+
run: |
239+
if ($env:OPENCVSHARP_RTSP_SERVER_PID) {
240+
Stop-Process -Id $env:OPENCVSHARP_RTSP_SERVER_PID -Force -ErrorAction SilentlyContinue
241+
}
242+
185243
- name: Upload crash dumps on failure
186244
if: failure()
187245
uses: actions/upload-artifact@v7

packaging/docker/manylinux/build_static_deps.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ dnf install -y nasm yasm
3535
# ---------------------------------------------------------------------------
3636
# FFmpeg (LGPL v2.1+ — statically linked, no patented external codecs)
3737
# Internal decoders cover H.264, H.265, VP8, VP9, MPEG-4, MPEG-2, and many others.
38+
# Networking remains enabled so videoio can open RTSP and other network streams.
3839
# Hwaccel autodetection (vaapi/vdpau/v4l2-m2m) is disabled explicitly: this build
3940
# never uses hardware acceleration, but if libva happens to be present in the
4041
# build container, FFmpeg's configure would auto-enable vaapi and silently add
@@ -59,7 +60,7 @@ cd "ffmpeg-${FFMPEG_VERSION}"
5960
--disable-doc \
6061
--disable-programs \
6162
--disable-debug \
62-
--disable-network \
63+
--enable-network \
6364
--disable-avdevice \
6465
--disable-postproc \
6566
--enable-avcodec \

test/OpenCvSharp.Tests/videoio/VideoCaptureTest.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ public class VideoCaptureTest : TestBase
1212
// True only when a real V4L2 device is present (e.g. /dev/video0)
1313
public static bool HasV4L2Device => IsLinux && Directory.EnumerateFiles("/dev", "video*").Any();
1414

15+
private const string RtspUrlEnvironmentVariable = "OPENCVSHARP_TEST_RTSP_URL";
16+
17+
// The CI workflows start a local MediaMTX instance and set this URL.
18+
public static bool HasRtspTestUrl =>
19+
!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(RtspUrlEnvironmentVariable));
20+
1521
// Backend used for the custom-stream (IStreamReader/Stream) VideoCapture tests below.
1622
// FFMPEG's custom-stream support (VideoCapture(Ptr<IStreamReader>, ...)) is flaky on the
1723
// Windows CI runners' prebuilt FFmpeg plugin DLL (createCapture(stream, ...) reports not
@@ -100,6 +106,30 @@ public void GrabAndRetrieveImageSequence()
100106
}
101107
}
102108

109+
[Fact(
110+
Skip = $"Set {RtspUrlEnvironmentVariable} to run the RTSP integration test",
111+
SkipUnless = nameof(HasRtspTestUrl))]
112+
public void ReadRtspStreamWithFFmpeg()
113+
{
114+
var url = Environment.GetEnvironmentVariable(RtspUrlEnvironmentVariable)!;
115+
using var capture = new VideoCapture(
116+
url,
117+
VideoCaptureAPIs.FFMPEG,
118+
[
119+
(int)VideoCaptureProperties.OpenTimeoutMsec, 10_000,
120+
(int)VideoCaptureProperties.ReadTimeoutMsec, 10_000,
121+
]);
122+
123+
Assert.True(capture.IsOpened());
124+
Assert.Equal("FFMPEG", capture.GetBackendName());
125+
126+
using var frame = new Mat();
127+
Assert.True(capture.Read(frame));
128+
Assert.False(frame.Empty());
129+
Assert.True(frame.Width > 0);
130+
Assert.True(frame.Height > 0);
131+
}
132+
103133
[Fact]
104134
public void GetSetExceptionMode()
105135
{

test/rtsp/mediamtx.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
logLevel: info
2+
rtspTransports: [tcp]
3+
rtmp: false
4+
hls: false
5+
webrtc: false
6+
srt: false
7+
moq: false
8+
9+
paths:
10+
test:
11+
alwaysAvailable: true
12+
alwaysAvailableTracks:
13+
- codec: H264

0 commit comments

Comments
 (0)