Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implemention of a push av server #36004

Draft
wants to merge 27 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8120c0e
Initial implemention of a push av server
fmonniot Oct 9, 2024
2bc6170
Initial implemention of a push av server
fmonniot Oct 9, 2024
e0d8388
restore global variable access
fmonniot Oct 16, 2024
7dc4a5f
fix cryptography warnings
fmonniot Oct 16, 2024
6f270f4
multiplex audio and video tracks
fmonniot Oct 16, 2024
f4ac5f8
Correct EKU based on hierarchy's kind
fmonniot Oct 22, 2024
905edc7
Fix listing stream files
fmonniot Oct 22, 2024
1695ca6
Run push av server in the background
fmonniot Oct 22, 2024
1b57e9c
change ca validity default + prep change to make it customizable
fmonniot Nov 7, 2024
e6dc11a
Attempt to provide a UI to browse certs/streams
fmonniot Nov 8, 2024
1afd43e
fix certificate handling of the cmaf generation script
fmonniot Nov 8, 2024
f499d93
poc UI for stream files
fmonniot Nov 8, 2024
e118231
lint and restyled
fmonniot Nov 8, 2024
6b3521e
Restyled by shfmt
restyled-commits Nov 8, 2024
ae8f652
Restyled by isort
restyled-commits Nov 8, 2024
f4d338c
fix API ordering so that getting a file details work
fmonniot Nov 27, 2024
01c5914
fix readme example
fmonniot Nov 27, 2024
8384867
Address code review feedback around certificate management
fmonniot Dec 11, 2024
fd7b8e2
add short example of using adhoc cert hierarchy
fmonniot Dec 11, 2024
f12149e
Restyled by isort
restyled-commits Dec 11, 2024
4be0913
Add support for the manifest upload
fmonniot Dec 11, 2024
353f187
fix variables in test
fmonniot Dec 11, 2024
8ce461d
make index redirect to streams ui
fmonniot Dec 11, 2024
4ecaca8
remove global variables
fmonniot Dec 11, 2024
89110fc
Make the push av server runnable in the background
fmonniot Dec 12, 2024
9c854e6
Document (and fix) certificate sign via API
fmonniot Dec 13, 2024
0ee9316
Restyled by isort
restyled-commits Dec 13, 2024
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
140 changes: 140 additions & 0 deletions src/python_testing/TC_PAVS_1_0.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import push_av_server
from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main


class TC_PAVS_1_0(MatterBaseTest):
"""
NOTE: this class is only a guide to understand what APIs I'd need to integrate in the push av server
for a better integration. It is not designed to be merged nor does it actually run (no chip module).
"""

def setup_class(self):
super().setup_class()

self.av_ctx = push_av_server.PushAvContext("127.0.0.1", 1234, None, "localhost")
self.av_ctx.start_in_background()

def teardown_class(self):
super().teardown_class()
self.av_ctx.terminate()

def steps_TC_PAVS_1_0(self):
return [TestStep(1, "Commissioning, already done", is_commissioning=True),
TestStep(2, "Install CA onto the device"),
TestStep(3, "Obtain device CSR, generate cert, provision cert onto device"),
TestStep(4, "Create media streams"),
TestStep(5, "Allocate push transport"),
TestStep(6, "Trigger a recording"),
TestStep(7, "Deallocate transport")
]

@async_test_body
async def test_TC_PAVS_1_0(self):
# commissioning - already done
self.step(1)

self.step(2)
# Access CA cert via the push_av_server package.
print(self.av_ctx.device_hierarchy.root_cert)
# read TLSCertificateManagament attributes to validate state
# Send the TLSCertificateManagament.ProvisionRootCertificate command
# Assert we got a response that contains a CA id
# read TLSCertificateManagament attributes to validate state

self.step(3)

self.step("3b")
# Generate nonce
# send TLSCertificateManagement.TLSClientCSR, receive TLSClientCSRResponse
print(self.av_ctx.device_hierarchy.gen_cert("device name", "csr"))
# send ProvisionClientCertificate, receive ProvisionClientCertificateResponse

self.step(4)
# (note: assum this step is a requirement and not the focus of these TCs)
# send VideoStreamAllocate, receive VideoStreamAllocateResponse
# StreamType: StreamTypeEnum.Recording
# VideoCodec: VideoCodecEnum.H264 (HEVC, VVC, AV1 are all optionals)
# MinFrameRate: 0
# MaxFrameRate: 60
# MinResolution: 0
# MaxResolution: 4k
# MinBitRate: 0
# MaxBitRate: inf
# MinFragmentLen: 0
# MaxFragmentLen: info
# send AudioStreamAllocate, receive AudioStreamAllocateResponse
# StreamType: StreamTypeEnum.Recording
# AudioCodec: AudioCodecEnum.OPUS (AAC-LC is optional)
# ChannelCount: 1 (note: or 2? what's the requirements that works for most cameras)
# SampleRate: TBD (48, 32, 16khz)
# BitRate: TBD
# BitDepth: TBD

self.step(5)
# send AllocatePushTransport, receive AllocatePushTransportResponse
# PushAVStreamTransportOptionsStruct:
# video stream id: from step 4
# audio stream id: from step 4
# tls endpoint id: from step 3b
# url: local dns + known path from step 2
# triggerOptions: PushAVStreamTransportMotionTriggerTimeControlStruct
# InitialDuration: default
# AugmentationDuration: default
# MaxDuration: default
# BlindDuration: default
# (note: are we testing this in this test plan or in webrtc?)
# containerFormat: PushAVStreamTransportContainerFormatEnum.CMAF (only one at the time)
# ingestMethod: PushAVStreamTransportIngestMethodEnum.CMAFIngest (only one at the time)
# containerOptions: PushAVStreamTransportContainerOptionsStruct
# ContainerType: PushAVStreamTransportContainerFormatEnum.CMAF (only one at the time)
# CMAFContainerOptions: PushAVStreamTransportCMAFContainerOptionsStruct
# ChunkDuration: default
# CENCKey: null. (note: do we test this in the harnes or do we not?)
# metadataOptions: PushAVStreamTransportMetadataOptionsStruct
# Multiplexing: PushAVStreamTransportStreamMultiplexingEnum.Interleaved
# IncludeMotionsZones: false
# EnablePrivacySensitive: false
# expiryTime: null? Not entirely sure how to test this one yet.

# find stream config and assert
# modify stream
# find stream config and assert

# set transport status
# find stream config and assert
# reset transport status
# find stream config and assert

self.step(6)
# subscribe to PushTransport events (note: forgot if it's required or not, I think it is)
# send ManuallyTriggerTransport
# ConnectionId: from step 5
# Action: PushAVStreamTransport_ActionEnum
# ActivationReason: PushAVStreamTransportTriggerActivationReasonEnum
# MinDuration: 5
# read
# listen for PushTransportStart and PushTransportEnd event
# wait for start event, validate conn id and options
# wait for end event, validate conn id and options

# Check metadata of stream sent to our web server
# ffmpeg convert the cmaf tracks into something more easily read by viewers
# manual step to inspect the video

self.step(7)
# TBD. deallocation logic

async def test_wrong_cert_chain(self):
"""Intended as an example of how to install an unrecognised """
wrong_chain = push_av_server.CAHierarchy(push_av_server.wd.mkdir("certs", "wrong"), "wrong chain", "client")

wrong_chain.root_cert_path # Install onto the device
[path_to_key, path_to_cert] = wrong_chain.gen_cert("dns", "csr") # Sign for the device
# Install onto the device
print(f"{path_to_cert}, {path_to_key}") # to not have unused values

# After setting up the device, trigger an upload which will use the wrong cert and should fail


if __name__ == "__main__":
default_matter_test_main()
1 change: 1 addition & 0 deletions src/python_testing/push_av_server.py
2 changes: 2 additions & 0 deletions src/tools/push_av_server/.flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[flake8]
max-line-length = 100
50 changes: 50 additions & 0 deletions src/tools/push_av_server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Push AV Server

This tool provide a web server that can be used to implement Matter cameras. The
server does not go out of its way to provide validation of the media ingested
(run the test harness to do so), but it does offer as much visibility as
possible on what the ingest source is sending to the server.

## Example

Here is an example of an interaction with the push AV server tool.

```sh
$ python server.py --working-directory ~/.pavstest


# First let's create a device key and certificate.
# The response will provide information as to where the key and certificate are located.
$ curl --cacert ~/.pavstest/certs/server/root.pem -XPOST https://localhost:1234/certs/dev/keypair

# Now that we have a device identity, we can create a stream
$ curl --cacert ~/.pavstest/certs/server/root.pem --cert ~/.pavstest/certs/device/dev.pem --key ~/.pavstest/certs/device/dev.key -XPOST https://localhost:1234/streams

# And now that we have access to our stream_id, we can build the publishing endpoint for
# any CMAF ingest flow we have. The example below assuming a stream id of "1".
$ export PUBLISHING_ENDPOINT=https://localhost:1234/streams/1

# The tool also contains a script to generate arbitrary CMAF content.
# This may be useful to implementers of a publish endpoint.
# This tool makes use of the previously created PUBLISHING_ENDPOINT environment variable.
# TODO Handle non-hardcoded client certificate
$ ./generate_cmaf_content.sh

# You can also list all streams and their associated files
$ curl -XGET --cacert ~/.pavstest/certs/server/root.pem https://localhost:1234/streams

# Get detailed information about the uploaded media file.
# This correspond to the ffprobe tool output
$ curl --cacert ~/.pavstest/certs/server/root.pem -XGET 'https://localhost:1234/probe/1/cmaf/example/video-720p.cmfv'

# You can also use the web server to sign certificates if given a CSR.
# First create a key and csr for your device:
$ openssl req -new -newkey rsa:2048 -nodes -keyout client.key -out client.csr -subj "/CN=test"

# When sending the CSR over JSON we need to have the newline characters be the literal \n.
$ sed '$!G' client.csr | paste -sd '\\n' - > client.curl.csr

# Then sign it with the server
$ curl --cacert ~/.pavstest/certs/server/root.pem -XPOST 'https://localhost:1234/certs/my-device/sign' -d "{\"csr\":\"$(cat client.curl.csr)\"}" --header "content-type: application/json"

```
65 changes: 65 additions & 0 deletions src/tools/push_av_server/generate_cmaf_content.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# source https://github.com/nagare-media/ingest/blob/main/scripts/tasks/run-cmaf-long-upload-ffmpeg
# Copyright 2022-2024 The nagare media authors under Apache 2.0

PUBLISHING_ENDPOINT=${PUBLISHING_ENDPOINT:-https://localhost:1234/stream/1}

# TODO Handle dynamic value for the certificates
CERT_ROOT_DIR=~/.pavstest

ffmpeg -hide_banner \
-re -f lavfi -i "
testsrc2=size=1280x720:rate=25,
drawbox=x=0:y=0:w=700:h=50:[email protected]:t=fill,
drawtext=x= 5:y=5:fontsize=54:fontcolor=white:text='%{pts\:gmtime\:$(date +%s)\:%Y-%m-%d}',
drawtext=x=345:y=5:fontsize=54:fontcolor=white:timecode='$(date -u '+%H\:%M\:%S')\:00':rate=25:tc24hmax=1,
setparams=field_mode=prog:range=tv:color_primaries=bt709:color_trc=bt709:colorspace=bt709,
format=yuv420p" \
-re -f lavfi -i "
sine=f=1000:r=48000:samples_per_frame='st(0,mod(n,5)); 1602-not(not(eq(ld(0),1)+eq(ld(0),3)))'" \
-shortest \
-fflags genpts \
\
-filter_complex "
[0:v]drawtext=x=(w-text_w)-5:y=5:fontsize=54:fontcolor=white:text='720p':box=1:[email protected]:boxborderw=5
" \
\
-c:a aac \
-b:a 64k \
-c:v libx264 \
-preset:v veryfast \
-tune zerolatency \
-profile:v main \
-crf:v 23 -bufsize:v:0 2250k -maxrate:v 2500k \
-g:v 100000 -keyint_min:v 50000 -force_key_frames:v "expr:gte(t,n_forced*2)" \
-x264opts no-open-gop=1 \
-bf 2 -b_strategy 2 -refs 1 \
-rc-lookahead 24 \
-export_side_data prft \
-field_order progressive -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv \
-pix_fmt yuv420p \
-f mp4 \
-frag_duration "$((1 * 1000 * 1000))" \
-min_frag_duration "$((1 * 1000 * 1000))" \
-write_prft wallclock \
-use_editlist 0 \
-movflags "+cmaf+dash+delay_moov+skip_sidx+skip_trailer+frag_custom" \
\
-method PUT \
-multiple_requests 1 \
-chunked_post 1 \
-send_expect_100 1 \
-headers "DASH-IF-Ingest: 1.1" \
-headers "Host: localhost:8080" \
-content_type "" \
-icy 0 \
-rw_timeout "$((200 * 1000 * 1000))" \
-reconnect 1 \
-reconnect_at_eof 1 \
-reconnect_on_network_error 1 \
-reconnect_on_http_error 4xx,5xx \
-reconnect_delay_max 2 \
-ca_file "$CERT_ROOT_DIR/certs/server/root.pem" \
-cert_file "$CERT_ROOT_DIR/certs/device/dev.pem" \
-key_file "$CERT_ROOT_DIR/certs/device/dev.key" \
-tls_verify 1 \
"$PUBLISHING_ENDPOINT/cmaf/example/video-720p.cmfv"
90 changes: 90 additions & 0 deletions src/tools/push_av_server/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8">
<title>Push AV Ref Server</title>
<script>
const tableFormat = (headers, gen) => {

let content = '<table><thead><tr>'

for (const header of headers) {
content += `<th>${header}</th>`
}

content += '</tr></thead><tbody>'

for (const iter of gen()) {
content += "<tr>"
for (const item of iter) {
content += `<td>${item}</td>`
}
content += "</tr>"
}

content += "</tbody></table>"

return content
}

document.addEventListener('DOMContentLoaded', () => {
const streamsTag = document.getElementById("streams")
const certsServerTag = document.getElementById("certs-server")
const certsDeviceTag = document.getElementById("certs-device")

fetch('/streams')
.then((r) => r.json())
.then((streams) => {
console.log('streams', streams)

let content = `
<table>
<tr>
<th>Stream ID</th>
<th>File</th>
</tr>
`

for (const { id, files } of streams.streams) {
for (const file of files) {
content += `<tr><td>${id}</td><td>${file}</td></tr>`
}
}

content += "</table>"

streamsTag.innerHTML = content
})

fetch('/certs')
.then((r) => r.json())
.then((certs) => {
console.log('certs', certs)

certsDeviceTag.innerHTML = tableFormat(['name'], function* () {
yield* certs.device.map((c) => [c])
})

certsServerTag.innerHTML = tableFormat(['name'], function* () {
yield* certs.server.map((c) => [c])
})
})
})
</script>
</head>

<body>
<h1>AV Push Server</h1>
<h2>Streams</h2>
<div id="streams"><!-- js generated --></div>
<h2>Certificates</h2>
<div>
<h3>Server certificates</h3>
<div id="certs-server"><!-- js generated --></div>
<h3>Device certificates</h3>
<div id="certs-device"><!-- js generated --></div>
</div>
</body>

</html>
5 changes: 5 additions & 0 deletions src/tools/push_av_server/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
zeroconf
cryptography
uvicorn
fastapi
jinja2
Loading
Loading