-
Notifications
You must be signed in to change notification settings - Fork 2k
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
fmonniot
wants to merge
2
commits into
project-chip:master
Choose a base branch
from
fmonniot:camera-push-av-server
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+971
−0
Draft
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../tools/push_av_server/server.py |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
[flake8] | ||
max-line-length = 100 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" \ | ||
\ | ||
-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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
zeroconf | ||
cryptography | ||
uvicorn | ||
fastapi |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.