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 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
130 changes: 130 additions & 0 deletions src/python_testing/TC_PAVS_1_0.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import push_av_server

import chip.clusters as Clusters
from chip.clusters import ClusterObjects as ClusterObjects
from matter_testing_support import (ClusterAttributeChangeAccumulator, MatterBaseTest, TestStep, default_matter_test_main,
async_test_body)
from mobly import asserts
from test_plan_support import commission_if_required, if_feature_supported, read_attribute, verify_success


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.
"""

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):
srv = push_av_server.start("localhost", 1234)
srv.run_in_thread()

# commissioning - already done
self.step(1)

self.step(2)
# Access CA cert via the push_av_server package.
push_av_server.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
push_av_server.device_hierarchy.gen_cert(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

srv.stop()


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
39 changes: 39 additions & 0 deletions src/tools/push_av_server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 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/streams/probe/1/cmaf/example.str/Switching(video)/video-720p.cmfv'
```
126 changes: 126 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,126 @@
# 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 those three variables
HTTP_OPTS=ca_file=~/.pavstest/certs/server/root.pem,cert_file=~/.pavstest/certs/device/dev.pem,key_file=~/.pavstest/certs/device/dev.key

ffmpeg -hide_banner \
-re -f lavfi -i "
testsrc2=size=1280x720:rate=25,
drawbox=x=0:y=0:w=700:h=50:c=black@.6: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:boxcolor=black@.6:boxborderw=5[v720p];
[0:v]drawtext=x=(w-text_w)-5:y=5:fontsize=54:fontcolor=white:text='360p':box=1:boxcolor=black@.6:boxborderw=5,scale=640x360[v360p]
" \
\
-map [v720p] \
-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 \
-http_opts $HTTP_OPTS \
"$PUBLISHING_ENDPOINT/cmaf/example.str/Switching(video)/video-720p.cmfv" \
Comment on lines +40 to +62
Copy link
Contributor

Choose a reason for hiding this comment

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

From the looks of this, this is generating non multiplexed tracks, aka video and audio are in seperate files instead of interleaved m4s / mp4 content.

Copy link
Author

Choose a reason for hiding this comment

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

Indeed. I missed the "All content SHALL be sent as a single stream and track." section of the Matter spec and was basing that example of off the DASH Ingest one, which I think do not limit the number of files. Will modify the script to generate one stream instead.

A question that comes to me from having only one stream, do we want to have the ecosystem provide the complete url as opposed to only the base? That may simplify the implementation on the ecosystem implementation, but would also restrict the spec if we want to expand it to include multiple streams in the future.

Copy link
Contributor

Choose a reason for hiding this comment

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

Based on my understanding of the DASH spec we are asking the ecosystem to provide the base URL and the camera then only appends the track name (always fixed since we are a single track) and segment number and file extension onto that to form the complete URL.
Right now the interleaved vs separate tracks is an enum in the spec that only defines interleaved (single track), so we have the ability to change in the future if we want.

\
-map [v360p] \
-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 \
-http_opts $HTTP_OPTS \
"$PUBLISHING_ENDPOINT/cmaf/example.str/Switching(video)/video-360p.cmfv" \
\
-map 1:a \
-c:a aac \
-b:a 64k \
-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 \
-http_opts $HTTP_OPTS \
"$PUBLISHING_ENDPOINT/cmaf/example.str/Switching(audio)/audio-64k.cmfa"
87 changes: 87 additions & 0 deletions src/tools/push_av_server/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<!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('/stream')
.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>
4 changes: 4 additions & 0 deletions src/tools/push_av_server/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
zeroconf
cryptography
uvicorn
fastapi
Loading
Loading