rtp: Add PCMU/PCMA RTP payloader / depayloader elements

These come with new generic RTP payloader, RTP raw-ish audio payloader
and RTP depayloader base classes.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1424>
This commit is contained in:
Sebastian Dröge 2023-10-14 11:23:56 +03:00
parent e09f9e9540
commit f563f8334b
18 changed files with 7930 additions and 41 deletions

89
Cargo.lock generated
View file

@ -2011,7 +2011,7 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "gio"
version = "0.20.0"
source = "git+https://github.com/gtk-rs/gtk-rs-core?branch=master#03c5a850037c582a0a5fca07f8e1cd1cf924c4fc"
source = "git+https://github.com/gtk-rs/gtk-rs-core?branch=master#90fc2033d4824e1c80a575d03b3c164e9901faf8"
dependencies = [
"futures-channel",
"futures-core",
@ -2028,7 +2028,7 @@ dependencies = [
[[package]]
name = "gio-sys"
version = "0.20.0"
source = "git+https://github.com/gtk-rs/gtk-rs-core?branch=master#03c5a850037c582a0a5fca07f8e1cd1cf924c4fc"
source = "git+https://github.com/gtk-rs/gtk-rs-core?branch=master#90fc2033d4824e1c80a575d03b3c164e9901faf8"
dependencies = [
"glib-sys",
"gobject-sys",
@ -2040,7 +2040,7 @@ dependencies = [
[[package]]
name = "glib"
version = "0.20.0"
source = "git+https://github.com/gtk-rs/gtk-rs-core?branch=master#03c5a850037c582a0a5fca07f8e1cd1cf924c4fc"
source = "git+https://github.com/gtk-rs/gtk-rs-core?branch=master#90fc2033d4824e1c80a575d03b3c164e9901faf8"
dependencies = [
"bitflags 2.4.2",
"futures-channel",
@ -2061,7 +2061,7 @@ dependencies = [
[[package]]
name = "glib-macros"
version = "0.20.0"
source = "git+https://github.com/gtk-rs/gtk-rs-core?branch=master#03c5a850037c582a0a5fca07f8e1cd1cf924c4fc"
source = "git+https://github.com/gtk-rs/gtk-rs-core?branch=master#90fc2033d4824e1c80a575d03b3c164e9901faf8"
dependencies = [
"heck",
"proc-macro-crate",
@ -2073,7 +2073,7 @@ dependencies = [
[[package]]
name = "glib-sys"
version = "0.20.0"
source = "git+https://github.com/gtk-rs/gtk-rs-core?branch=master#03c5a850037c582a0a5fca07f8e1cd1cf924c4fc"
source = "git+https://github.com/gtk-rs/gtk-rs-core?branch=master#90fc2033d4824e1c80a575d03b3c164e9901faf8"
dependencies = [
"libc",
"system-deps",
@ -2088,7 +2088,7 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "gobject-sys"
version = "0.20.0"
source = "git+https://github.com/gtk-rs/gtk-rs-core?branch=master#03c5a850037c582a0a5fca07f8e1cd1cf924c4fc"
source = "git+https://github.com/gtk-rs/gtk-rs-core?branch=master#90fc2033d4824e1c80a575d03b3c164e9901faf8"
dependencies = [
"glib-sys",
"libc",
@ -2630,13 +2630,18 @@ dependencies = [
name = "gst-plugin-rtp"
version = "0.13.0-alpha.1"
dependencies = [
"atomic_refcell",
"bitstream-io",
"chrono",
"gst-plugin-version-helper",
"gstreamer",
"gstreamer-app",
"gstreamer-check",
"gstreamer-rtp",
"once_cell",
"rand",
"rtp-types",
"smallvec",
]
[[package]]
@ -2954,7 +2959,7 @@ dependencies = [
[[package]]
name = "gstreamer"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"cfg-if",
"futures-channel",
@ -2980,7 +2985,7 @@ dependencies = [
[[package]]
name = "gstreamer-app"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"futures-core",
"futures-sink",
@ -2994,7 +2999,7 @@ dependencies = [
[[package]]
name = "gstreamer-app-sys"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib-sys",
"gstreamer-base-sys",
@ -3006,7 +3011,7 @@ dependencies = [
[[package]]
name = "gstreamer-audio"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"cfg-if",
"glib",
@ -3021,7 +3026,7 @@ dependencies = [
[[package]]
name = "gstreamer-audio-sys"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib-sys",
"gobject-sys",
@ -3034,7 +3039,7 @@ dependencies = [
[[package]]
name = "gstreamer-base"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"atomic_refcell",
"cfg-if",
@ -3047,7 +3052,7 @@ dependencies = [
[[package]]
name = "gstreamer-base-sys"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib-sys",
"gobject-sys",
@ -3059,7 +3064,7 @@ dependencies = [
[[package]]
name = "gstreamer-check"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib",
"gstreamer",
@ -3069,7 +3074,7 @@ dependencies = [
[[package]]
name = "gstreamer-check-sys"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib-sys",
"gobject-sys",
@ -3081,7 +3086,7 @@ dependencies = [
[[package]]
name = "gstreamer-gl"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib",
"gstreamer",
@ -3095,7 +3100,7 @@ dependencies = [
[[package]]
name = "gstreamer-gl-egl"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib",
"gstreamer",
@ -3107,7 +3112,7 @@ dependencies = [
[[package]]
name = "gstreamer-gl-egl-sys"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib-sys",
"gstreamer-gl-sys",
@ -3118,7 +3123,7 @@ dependencies = [
[[package]]
name = "gstreamer-gl-sys"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib-sys",
"gobject-sys",
@ -3132,7 +3137,7 @@ dependencies = [
[[package]]
name = "gstreamer-gl-wayland"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib",
"gstreamer",
@ -3144,7 +3149,7 @@ dependencies = [
[[package]]
name = "gstreamer-gl-wayland-sys"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib-sys",
"gstreamer-gl-sys",
@ -3155,7 +3160,7 @@ dependencies = [
[[package]]
name = "gstreamer-gl-x11"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib",
"gstreamer",
@ -3167,7 +3172,7 @@ dependencies = [
[[package]]
name = "gstreamer-gl-x11-sys"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib-sys",
"gstreamer-gl-sys",
@ -3178,7 +3183,7 @@ dependencies = [
[[package]]
name = "gstreamer-net"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"gio",
"glib",
@ -3189,7 +3194,7 @@ dependencies = [
[[package]]
name = "gstreamer-net-sys"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"gio-sys",
"glib-sys",
@ -3201,7 +3206,7 @@ dependencies = [
[[package]]
name = "gstreamer-pbutils"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib",
"gstreamer",
@ -3215,7 +3220,7 @@ dependencies = [
[[package]]
name = "gstreamer-pbutils-sys"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib-sys",
"gobject-sys",
@ -3229,7 +3234,7 @@ dependencies = [
[[package]]
name = "gstreamer-rtp"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib",
"gstreamer",
@ -3240,7 +3245,7 @@ dependencies = [
[[package]]
name = "gstreamer-rtp-sys"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib-sys",
"gstreamer-base-sys",
@ -3252,7 +3257,7 @@ dependencies = [
[[package]]
name = "gstreamer-sdp"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib",
"gstreamer",
@ -3262,7 +3267,7 @@ dependencies = [
[[package]]
name = "gstreamer-sdp-sys"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib-sys",
"gstreamer-sys",
@ -3273,7 +3278,7 @@ dependencies = [
[[package]]
name = "gstreamer-sys"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib-sys",
"gobject-sys",
@ -3284,7 +3289,7 @@ dependencies = [
[[package]]
name = "gstreamer-utils"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"gstreamer",
"gstreamer-app",
@ -3296,7 +3301,7 @@ dependencies = [
[[package]]
name = "gstreamer-video"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"cfg-if",
"futures-channel",
@ -3313,7 +3318,7 @@ dependencies = [
[[package]]
name = "gstreamer-video-sys"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib-sys",
"gobject-sys",
@ -3326,7 +3331,7 @@ dependencies = [
[[package]]
name = "gstreamer-webrtc"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib",
"gstreamer",
@ -3338,7 +3343,7 @@ dependencies = [
[[package]]
name = "gstreamer-webrtc-sys"
version = "0.23.0"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#b10f395c2cf0dc6d1af35068c0960ea5b839158d"
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs?branch=main#28fe70f479fcb2f22971fc29a3b81e1cef097578"
dependencies = [
"glib-sys",
"gstreamer-sdp-sys",
@ -5390,6 +5395,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "rtp-types"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01b38bb7fd9425628876786934ade84ec5cb63905804f073583e6554d33f9af"
dependencies = [
"smallvec",
"thiserror",
]
[[package]]
name = "rtsp-types"
version = "0.1.1"

View file

@ -6311,11 +6311,548 @@
}
},
"rank": "none"
},
"rtppcmadepay2": {
"author": "Sebastian Dröge <sebastian@centricular.com>",
"description": "Depayload A-law from RTP packets (RFC 3551)",
"hierarchy": [
"GstRtpPcmaDepay2",
"GstRtpPcmauDepay2",
"GstRtpBaseDepay2",
"GstElement",
"GstObject",
"GInitiallyUnowned",
"GObject"
],
"klass": "Codec/Depayloader/Network/RTP",
"pad-templates": {
"sink": {
"caps": "application/x-rtp:\n media: audio\n payload: 8\n clock-rate: 8000\napplication/x-rtp:\n media: audio\n clock-rate: [ 1, 2147483647 ]\n encoding-name: PCMA\n",
"direction": "sink",
"presence": "always"
},
"src": {
"caps": "audio/x-alaw:\n channels: 1\n rate: [ 1, 2147483647 ]\n",
"direction": "src",
"presence": "always"
}
},
"rank": "marginal"
},
"rtppcmapay2": {
"author": "Sebastian Dröge <sebastian@centricular.com>",
"description": "Payload A-law Audio into RTP packets (RFC 3551)",
"hierarchy": [
"GstRtpPcmaPay2",
"GstRtpPcmauPay2",
"GstRtpBaseAudioPay2",
"GstRtpBasePay2",
"GstElement",
"GstObject",
"GInitiallyUnowned",
"GObject"
],
"klass": "Codec/Payloader/Network/RTP",
"pad-templates": {
"sink": {
"caps": "audio/x-alaw:\n channels: 1\n rate: [ 1, 2147483647 ]\n",
"direction": "sink",
"presence": "always"
},
"src": {
"caps": "application/x-rtp:\n media: audio\n payload: 8\n clock-rate: 8000\napplication/x-rtp:\n media: audio\n payload: [ 96, 127 ]\n encoding-name: PCMA\n clock-rate: [ 1, 2147483647 ]\n",
"direction": "src",
"presence": "always"
}
},
"rank": "marginal"
},
"rtppcmudepay2": {
"author": "Sebastian Dröge <sebastian@centricular.com>",
"description": "Depayload µ-law from RTP packets (RFC 3551)",
"hierarchy": [
"GstRtpPcmuDepay2",
"GstRtpPcmauDepay2",
"GstRtpBaseDepay2",
"GstElement",
"GstObject",
"GInitiallyUnowned",
"GObject"
],
"klass": "Codec/Depayloader/Network/RTP",
"pad-templates": {
"sink": {
"caps": "application/x-rtp:\n media: audio\n payload: 0\n clock-rate: 8000\napplication/x-rtp:\n media: audio\n clock-rate: [ 1, 2147483647 ]\n encoding-name: PCMU\n",
"direction": "sink",
"presence": "always"
},
"src": {
"caps": "audio/x-mulaw:\n channels: 1\n rate: [ 1, 2147483647 ]\n",
"direction": "src",
"presence": "always"
}
},
"rank": "marginal"
},
"rtppcmupay2": {
"author": "Sebastian Dröge <sebastian@centricular.com>",
"description": "Payload µ-law Audio into RTP packets (RFC 3551)",
"hierarchy": [
"GstRtpPcmuPay2",
"GstRtpPcmauPay2",
"GstRtpBaseAudioPay2",
"GstRtpBasePay2",
"GstElement",
"GstObject",
"GInitiallyUnowned",
"GObject"
],
"klass": "Codec/Payloader/Network/RTP",
"pad-templates": {
"sink": {
"caps": "audio/x-mulaw:\n channels: 1\n rate: [ 1, 2147483647 ]\n",
"direction": "sink",
"presence": "always"
},
"src": {
"caps": "application/x-rtp:\n media: audio\n payload: 0\n clock-rate: 8000\napplication/x-rtp:\n media: audio\n payload: [ 96, 127 ]\n encoding-name: PCMU\n clock-rate: [ 1, 2147483647 ]\n",
"direction": "src",
"presence": "always"
}
},
"rank": "marginal"
}
},
"filename": "gstrsrtp",
"license": "MPL-2.0",
"other-types": {},
"other-types": {
"GstRtpBaseAudioPay2": {
"hierarchy": [
"GstRtpBaseAudioPay2",
"GstRtpBasePay2",
"GstElement",
"GstObject",
"GInitiallyUnowned",
"GObject"
],
"kind": "object",
"properties": {
"alignment-threshold": {
"blurb": "Timestamp alignment threshold in nanoseconds",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "40000000",
"max": "18446744073709551615",
"min": "0",
"mutable": "playing",
"readable": true,
"type": "guint64",
"writable": true
},
"discont-wait": {
"blurb": "Window of time in nanoseconds to wait before creating a discontinuity",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "1000000000",
"max": "18446744073709551615",
"min": "0",
"mutable": "playing",
"readable": true,
"type": "guint64",
"writable": true
},
"max-ptime": {
"blurb": "Maximum duration of the packet data in ns (-1 = unlimited up to MTU)",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "18446744073709551615",
"max": "9223372036854775807",
"min": "-1",
"mutable": "playing",
"readable": true,
"type": "gint64",
"writable": true
},
"min-ptime": {
"blurb": "Minimum duration of the packet data in ns (can't go above MTU)",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "0",
"max": "9223372036854775807",
"min": "0",
"mutable": "playing",
"readable": true,
"type": "gint64",
"writable": true
},
"ptime-multiple": {
"blurb": "Force buffers to be multiples of this duration in ns (0 disables)",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "0",
"max": "9223372036854775807",
"min": "0",
"mutable": "playing",
"readable": true,
"type": "gint64",
"writable": true
}
}
},
"GstRtpBaseDepay2": {
"hierarchy": [
"GstRtpBaseDepay2",
"GstElement",
"GstObject",
"GInitiallyUnowned",
"GObject"
],
"kind": "object",
"properties": {
"auto-header-extensions": {
"blurb": "Whether RTP header extensions should be automatically enabled, if an implementation is available",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "true",
"mutable": "ready",
"readable": true,
"type": "gboolean",
"writable": true
},
"extensions": {
"blurb": "List of enabled RTP header extensions",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"mutable": "null",
"readable": true,
"type": "GstValueArray",
"writable": false
},
"max-reorder": {
"blurb": "Maximum seqnum reorder before assuming sender has restarted",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "100",
"max": "32767",
"min": "0",
"mutable": "playing",
"readable": true,
"type": "guint",
"writable": true
},
"source-info": {
"blurb": "Add RTP source information as buffer metadata",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "false",
"mutable": "playing",
"readable": true,
"type": "gboolean",
"writable": true
},
"stats": {
"blurb": "Various statistics",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "application/x-rtp-depayload-stats;",
"mutable": "null",
"readable": true,
"type": "GstStructure",
"writable": false
}
},
"signals": {
"add-extension": {
"action": true,
"args": [
{
"name": "arg0",
"type": "GstRTPHeaderExtension"
}
],
"return-type": "void",
"when": "last"
},
"clear-extensions": {
"action": true,
"args": [],
"return-type": "void",
"when": "last"
},
"request-extension": {
"args": [
{
"name": "arg0",
"type": "guint"
},
{
"name": "arg1",
"type": "gchararray"
}
],
"return-type": "GstRTPHeaderExtension",
"when": "last"
}
}
},
"GstRtpBasePay2": {
"hierarchy": [
"GstRtpBasePay2",
"GstElement",
"GstObject",
"GInitiallyUnowned",
"GObject"
],
"kind": "object",
"properties": {
"auto-header-extensions": {
"blurb": "Whether RTP header extensions should be automatically enabled, if an implementation is available",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "true",
"mutable": "ready",
"readable": true,
"type": "gboolean",
"writable": true
},
"extensions": {
"blurb": "List of enabled RTP header extensions",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"mutable": "null",
"readable": true,
"type": "GstValueArray",
"writable": false
},
"mtu": {
"blurb": "Maximum size of one RTP packet",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "1400",
"max": "-1",
"min": "28",
"mutable": "playing",
"readable": true,
"type": "guint",
"writable": true
},
"onvif-no-rate-control": {
"blurb": "Enable ONVIF Rate-Control=no timestamping mode",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "false",
"mutable": "ready",
"readable": true,
"type": "gboolean",
"writable": true
},
"pt": {
"blurb": "Payload type of the packets",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "8",
"max": "127",
"min": "0",
"mutable": "ready",
"readable": true,
"type": "guint",
"writable": true
},
"scale-rtptime": {
"blurb": "Whether the RTP timestamp should be scaled with the rate (speed)",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "true",
"mutable": "ready",
"readable": true,
"type": "gboolean",
"writable": true
},
"seqnum": {
"blurb": "RTP sequence number of the last packet",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "0",
"max": "65535",
"min": "0",
"mutable": "null",
"readable": true,
"type": "guint",
"writable": false
},
"seqnum-offset": {
"blurb": "Offset that is added to all RTP sequence numbers (-1 == random)",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "-1",
"max": "65535",
"min": "-1",
"mutable": "ready",
"readable": true,
"type": "gint",
"writable": true
},
"source-info": {
"blurb": "Add RTP source information as buffer metadata",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "false",
"mutable": "playing",
"readable": true,
"type": "gboolean",
"writable": true
},
"ssrc": {
"blurb": "SSRC of the packets (-1 == random)",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "18446744073709551615",
"max": "4294967295",
"min": "-1",
"mutable": "ready",
"readable": true,
"type": "gint64",
"writable": true
},
"stats": {
"blurb": "Various statistics",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "application/x-rtp-payload-stats;",
"mutable": "null",
"readable": true,
"type": "GstStructure",
"writable": false
},
"timestamp": {
"blurb": "RTP timestamp of the last packet",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "0",
"max": "65535",
"min": "0",
"mutable": "null",
"readable": true,
"type": "guint",
"writable": false
},
"timestamp-offset": {
"blurb": "Offset that is added to all RTP timestamps (-1 == random)",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "18446744073709551615",
"max": "4294967295",
"min": "-1",
"mutable": "ready",
"readable": true,
"type": "gint64",
"writable": true
}
},
"signals": {
"add-extension": {
"action": true,
"args": [
{
"name": "arg0",
"type": "GstRTPHeaderExtension"
}
],
"return-type": "void",
"when": "last"
},
"clear-extensions": {
"action": true,
"args": [],
"return-type": "void",
"when": "last"
},
"request-extension": {
"args": [
{
"name": "arg0",
"type": "guint"
},
{
"name": "arg1",
"type": "gchararray"
}
],
"return-type": "GstRTPHeaderExtension",
"when": "last"
}
}
},
"GstRtpPcmauDepay2": {
"hierarchy": [
"GstRtpPcmauDepay2",
"GstRtpBaseDepay2",
"GstElement",
"GstObject",
"GInitiallyUnowned",
"GObject"
],
"kind": "object"
},
"GstRtpPcmauPay2": {
"hierarchy": [
"GstRtpPcmauPay2",
"GstRtpBaseAudioPay2",
"GstRtpBasePay2",
"GstElement",
"GstObject",
"GInitiallyUnowned",
"GObject"
],
"kind": "object"
}
},
"package": "gst-plugin-rtp",
"source": "gst-plugin-rtp",
"tracers": {},

View file

@ -9,14 +9,19 @@ description = "GStreamer Rust RTP Plugin"
rust-version.workspace = true
[dependencies]
bitstream-io = "2.0"
gst = { workspace = true, features = ["v1_20"] }
gst-rtp = { workspace = true, features = ["v1_20"]}
atomic_refcell = "0.1"
bitstream-io = "2.1"
chrono = { version = "0.4", default-features = false }
gst = { workspace = true, features = ["v1_20"] }
gst-rtp = { workspace = true, features = ["v1_20"] }
once_cell.workspace = true
rand = { version = "0.8", default-features = false, features = ["std", "std_rng" ] }
rtp-types = { version = "0.1" }
smallvec = { version = "1.11", features = ["union", "write", "const_generics", "const_new"] }
[dev-dependencies]
gst-check = { workspace = true, features = ["v1_20"] }
gst-app = { workspace = true, features = ["v1_20"] }
[build-dependencies]
gst-plugin-version-helper.workspace = true

View file

@ -0,0 +1,193 @@
// SPDX-License-Identifier: MPL-2.0
use gst::{
glib::{self, prelude::*},
prelude::*,
};
#[derive(Debug, Default)]
pub struct AudioDiscont {
/// If last processing detected a discontinuity.
discont_pending: bool,
/// Base PTS to which the offsets below are relative.
base_pts: Option<gst::ClockTime>,
/// Next output sample offset, i.e. offset of the first sample of the queued buffers.
///
/// This is only set once the packet with the base PTS is output.
next_out_offset: Option<u64>,
/// Next expected input sample offset.
next_in_offset: Option<u64>,
/// PTS of the last buffer that was above the alignment threshold.
///
/// This is reset whenever the next buffer is actually below the alignment threshold again.
/// FIXME: Should this be running time?
discont_time: Option<gst::ClockTime>,
/// Last known sample rate.
last_rate: Option<u32>,
}
impl AudioDiscont {
pub fn process_input(
&mut self,
settings: &AudioDiscontConfiguration,
discont: bool,
rate: u32,
pts: gst::ClockTime,
num_samples: usize,
) -> bool {
if self.discont_pending {
return true;
}
if self.last_rate.map_or(false, |last_rate| last_rate != rate) {
self.discont_pending = true;
}
self.last_rate = Some(rate);
if discont {
self.discont_pending = true;
return true;
}
// If we have no base PTS yet, this is the first buffer and there's a discont
let Some(base_pts) = self.base_pts else {
self.discont_pending = true;
return true;
};
// Never detect a discont if alignment threshold is not set
let Some(alignment_threshold) = settings.alignment_threshold else {
return false;
};
let expected_pts = base_pts
+ gst::ClockTime::from_nseconds(
self.next_in_offset
.unwrap_or(0)
.mul_div_ceil(gst::ClockTime::SECOND.nseconds(), rate as u64)
.unwrap(),
);
let mut discont = false;
let diff = pts.into_positive() - expected_pts.into_positive();
if diff.abs() >= alignment_threshold {
let mut resync = false;
if settings.discont_wait.is_zero() {
resync = true;
} else if let Some(discont_time) = self.discont_time {
if (discont_time.into_positive() - pts.into_positive()).abs()
>= settings.discont_wait.into_positive()
{
resync = true;
}
} else if (expected_pts.into_positive() - pts.into_positive()).abs()
>= settings.discont_wait.into_positive()
{
resync = true;
} else {
self.discont_time = Some(expected_pts);
}
if resync {
discont = true;
}
} else {
self.discont_time = None;
}
self.next_in_offset = Some(self.next_in_offset.unwrap_or(0) + num_samples as u64);
if discont {
self.discont_pending = true;
}
discont
}
pub fn resync(&mut self, base_pts: gst::ClockTime, num_samples: usize) {
self.discont_pending = false;
self.base_pts = Some(base_pts);
self.next_in_offset = Some(num_samples as u64);
self.next_out_offset = None;
self.discont_time = None;
self.last_rate = None;
}
pub fn reset(&mut self) {
*self = AudioDiscont::default();
}
pub fn base_pts(&self) -> Option<gst::ClockTime> {
self.base_pts
}
pub fn next_output_offset(&self) -> Option<u64> {
self.next_out_offset
}
pub fn process_output(&mut self, num_samples: usize) {
self.next_out_offset = Some(self.next_out_offset.unwrap_or(0) + num_samples as u64);
}
}
#[derive(Debug, Copy, Clone)]
pub struct AudioDiscontConfiguration {
pub alignment_threshold: Option<gst::ClockTime>,
pub discont_wait: gst::ClockTime,
}
impl Default for AudioDiscontConfiguration {
fn default() -> Self {
AudioDiscontConfiguration {
alignment_threshold: Some(gst::ClockTime::from_mseconds(40)),
discont_wait: gst::ClockTime::from_seconds(1),
}
}
}
impl AudioDiscontConfiguration {
pub fn create_pspecs() -> Vec<glib::ParamSpec> {
vec![
glib::ParamSpecUInt64::builder("alignment-threshold")
.nick("Alignment Threshold")
.blurb("Timestamp alignment threshold in nanoseconds")
.default_value(
Self::default()
.alignment_threshold
.map(gst::ClockTime::nseconds)
.unwrap_or(u64::MAX),
)
.mutable_playing()
.build(),
glib::ParamSpecUInt64::builder("discont-wait")
.nick("Discont Wait")
.blurb("Window of time in nanoseconds to wait before creating a discontinuity")
.default_value(Self::default().discont_wait.nseconds())
.mutable_playing()
.build(),
]
}
pub fn set_property(&mut self, value: &glib::Value, pspec: &glib::ParamSpec) -> bool {
match pspec.name() {
"alignment-threshold" => {
self.alignment_threshold = value.get().unwrap();
true
}
"discont-wait" => {
self.discont_wait = value.get().unwrap();
true
}
_ => false,
}
}
pub fn property(&self, pspec: &glib::ParamSpec) -> Option<glib::Value> {
match pspec.name() {
"alignment-threshold" => Some(self.alignment_threshold.to_value()),
"discont-wait" => Some(self.discont_wait.to_value()),
_ => None,
}
}
}

View file

@ -0,0 +1,518 @@
//
// Copyright (C) 2023 Sebastian Dröge <sebastian@centricular.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0
use std::{cmp, collections::VecDeque, sync::Mutex};
use atomic_refcell::AtomicRefCell;
use gst::{glib, prelude::*, subclass::prelude::*};
use once_cell::sync::Lazy;
use crate::{
audio_discont::{AudioDiscont, AudioDiscontConfiguration},
basepay::{PacketToBufferRelation, RtpBasePay2Ext, RtpBasePay2ImplExt, TimestampOffset},
};
#[derive(Clone)]
struct Settings {
max_ptime: Option<gst::ClockTime>,
min_ptime: gst::ClockTime,
ptime_multiple: gst::ClockTime,
audio_discont: AudioDiscontConfiguration,
}
impl Default for Settings {
fn default() -> Self {
Settings {
max_ptime: None,
min_ptime: gst::ClockTime::ZERO,
ptime_multiple: gst::ClockTime::ZERO,
audio_discont: AudioDiscontConfiguration::default(),
}
}
}
struct QueuedBuffer {
/// ID of the buffer.
id: u64,
/// The mapped buffer itself.
buffer: gst::MappedBuffer<gst::buffer::Readable>,
/// Offset into the buffer that was not consumed yet.
offset: usize,
}
#[derive(Default)]
struct State {
/// Currently configured clock rate.
clock_rate: Option<u32>,
/// Number of bytes per frame.
bpf: Option<usize>,
/// Desired "packet time", i.e. packet duration, from the caps, if set.
ptime: Option<gst::ClockTime>,
max_ptime: Option<gst::ClockTime>,
/// Currently queued buffers.
queued_buffers: VecDeque<QueuedBuffer>,
/// Currently queued number of bytes.
queued_bytes: usize,
audio_discont: AudioDiscont,
}
#[derive(Default)]
pub struct RtpBaseAudioPay2 {
settings: Mutex<Settings>,
state: AtomicRefCell<State>,
}
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
gst::DebugCategory::new(
"rtpbaseaudiopay2",
gst::DebugColorFlags::empty(),
Some("Base RTP Audio Payloader"),
)
});
#[glib::object_subclass]
impl ObjectSubclass for RtpBaseAudioPay2 {
const ABSTRACT: bool = true;
const NAME: &'static str = "GstRtpBaseAudioPay2";
type Type = super::RtpBaseAudioPay2;
type ParentType = crate::basepay::RtpBasePay2;
}
impl ObjectImpl for RtpBaseAudioPay2 {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
let mut properties = vec![
// Using same type/semantics as C payloaders
glib::ParamSpecInt64::builder("max-ptime")
.nick("Maximum Packet Time")
.blurb("Maximum duration of the packet data in ns (-1 = unlimited up to MTU)")
.default_value(
Settings::default()
.max_ptime
.map(gst::ClockTime::nseconds)
.map(|x| x as i64)
.unwrap_or(-1),
)
.minimum(-1)
.maximum(i64::MAX)
.mutable_playing()
.build(),
// Using same type/semantics as C payloaders
glib::ParamSpecInt64::builder("min-ptime")
.nick("Minimum Packet Time")
.blurb("Minimum duration of the packet data in ns (can't go above MTU)")
.default_value(Settings::default().min_ptime.nseconds() as i64)
.minimum(0)
.maximum(i64::MAX)
.mutable_playing()
.build(),
// Using same type/semantics as C payloaders
glib::ParamSpecInt64::builder("ptime-multiple")
.nick("Packet Time Multiple")
.blurb("Force buffers to be multiples of this duration in ns (0 disables)")
.default_value(Settings::default().ptime_multiple.nseconds() as i64)
.minimum(0)
.maximum(i64::MAX)
.mutable_playing()
.build(),
];
properties.extend_from_slice(&AudioDiscontConfiguration::create_pspecs());
properties
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
if self
.settings
.lock()
.unwrap()
.audio_discont
.set_property(value, pspec)
{
return;
}
match pspec.name() {
"max-ptime" => {
let v = value.get::<i64>().unwrap();
self.settings.lock().unwrap().max_ptime =
(v != -1).then_some(gst::ClockTime::from_nseconds(v as u64));
}
"min-ptime" => {
let v = gst::ClockTime::from_nseconds(value.get::<i64>().unwrap() as u64);
let mut settings = self.settings.lock().unwrap();
let changed = settings.min_ptime != v;
settings.min_ptime = v;
drop(settings);
if changed {
let _ = self
.obj()
.post_message(gst::message::Latency::builder().src(&*self.obj()).build());
}
}
"ptime-multiple" => {
self.settings.lock().unwrap().ptime_multiple =
gst::ClockTime::from_nseconds(value.get::<i64>().unwrap() as u64);
}
_ => unimplemented!(),
};
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
if let Some(value) = self.settings.lock().unwrap().audio_discont.property(pspec) {
return value;
}
match pspec.name() {
"max-ptime" => (self
.settings
.lock()
.unwrap()
.max_ptime
.map(gst::ClockTime::nseconds)
.map(|x| x as i64)
.unwrap_or(-1))
.to_value(),
"min-ptime" => (self.settings.lock().unwrap().min_ptime.nseconds() as i64).to_value(),
"ptime-multiple" => {
(self.settings.lock().unwrap().ptime_multiple.nseconds() as i64).to_value()
}
_ => unimplemented!(),
}
}
}
impl GstObjectImpl for RtpBaseAudioPay2 {}
impl ElementImpl for RtpBaseAudioPay2 {}
impl crate::basepay::RtpBasePay2Impl for RtpBaseAudioPay2 {
const ALLOWED_META_TAGS: &'static [&'static str] = &["audio"];
fn start(&self) -> Result<(), gst::ErrorMessage> {
*self.state.borrow_mut() = State::default();
Ok(())
}
fn stop(&self) -> Result<(), gst::ErrorMessage> {
*self.state.borrow_mut() = State::default();
Ok(())
}
fn negotiate(&self, mut src_caps: gst::Caps) {
// Fixate here as a first step
src_caps.fixate();
let s = src_caps.structure(0).unwrap();
// Negotiate ptime/maxptime with downstream and use them in combination with the
// properties. See https://datatracker.ietf.org/doc/html/rfc4566#section-6
let ptime = s
.get::<u32>("ptime")
.ok()
.map(u64::from)
.map(gst::ClockTime::from_mseconds);
let max_ptime = s
.get::<u32>("maxptime")
.ok()
.map(u64::from)
.map(gst::ClockTime::from_mseconds);
let clock_rate = match s.get::<i32>("clock-rate") {
Ok(clock_rate) if clock_rate > 0 => clock_rate as u32,
_ => {
panic!("RTP caps {src_caps:?} without 'clock-rate'");
}
};
self.parent_negotiate(src_caps);
// Draining happened above if the clock rate has changed
let mut state = self.state.borrow_mut();
state.ptime = ptime;
state.max_ptime = max_ptime;
state.clock_rate = Some(clock_rate);
drop(state);
}
fn drain(&self) -> Result<gst::FlowSuccess, gst::FlowError> {
let settings = self.settings.lock().unwrap().clone();
let mut state = self.state.borrow_mut();
self.drain_packets(&settings, &mut state, true)
}
fn flush(&self) {
let mut state = self.state.borrow_mut();
state.queued_buffers.clear();
state.queued_bytes = 0;
state.audio_discont.reset();
}
fn handle_buffer(
&self,
buffer: &gst::Buffer,
id: u64,
) -> Result<gst::FlowSuccess, gst::FlowError> {
let buffer = buffer.clone().into_mapped_buffer_readable().map_err(|_| {
gst::error!(CAT, imp: self, "Can't map buffer readable");
gst::FlowError::Error
})?;
let pts = buffer.buffer().pts().unwrap();
let settings = self.settings.lock().unwrap().clone();
let mut state = self.state.borrow_mut();
let Some(bpf) = state.bpf else {
return Err(gst::FlowError::NotNegotiated);
};
let Some(clock_rate) = state.clock_rate else {
return Err(gst::FlowError::NotNegotiated);
};
let num_samples = buffer.size() / bpf;
let discont = state.audio_discont.process_input(
&settings.audio_discont,
buffer.buffer().flags().contains(gst::BufferFlags::DISCONT),
clock_rate,
pts,
num_samples,
);
if discont {
if state.audio_discont.base_pts().is_some() {
gst::debug!(CAT, imp: self, "Draining because of discontinuity");
self.drain_packets(&settings, &mut state, true)?;
}
state.audio_discont.resync(pts, num_samples);
}
state.queued_bytes += buffer.buffer().size();
state.queued_buffers.push_back(QueuedBuffer {
id,
buffer,
offset: 0,
});
self.drain_packets(&settings, &mut state, false)
}
#[allow(clippy::single_match)]
fn src_query(&self, query: &mut gst::QueryRef) -> bool {
let res = self.parent_src_query(query);
if !res {
return false;
}
match query.view_mut() {
gst::QueryViewMut::Latency(query) => {
let (is_live, mut min, mut max) = query.result();
let min_ptime = self.settings.lock().unwrap().min_ptime;
min += min_ptime;
max.opt_add_assign(min_ptime);
query.set(is_live, min, max);
}
_ => (),
}
true
}
}
impl RtpBaseAudioPay2 {
/// Returns the minimum, maximum and chunk/multiple packet sizes
fn calculate_packet_sizes(
&self,
settings: &Settings,
state: &State,
clock_rate: u32,
bpf: usize,
) -> (usize, usize, usize) {
let min_pframes = settings
.min_ptime
.nseconds()
.mul_div_ceil(clock_rate as u64, gst::ClockTime::SECOND.nseconds())
.unwrap() as u32;
let max_ptime = match (settings.max_ptime, state.max_ptime) {
(Some(max_ptime), Some(caps_max_ptime)) => Some(cmp::min(max_ptime, caps_max_ptime)),
(None, Some(max_ptime)) => Some(max_ptime),
(Some(max_ptime), None) => Some(max_ptime),
_ => None,
};
let max_pframes = max_ptime.map(|max_ptime| {
max_ptime
.nseconds()
.mul_div_ceil(clock_rate as u64, gst::ClockTime::SECOND.nseconds())
.unwrap() as u32
});
let pframes_multiple = cmp::max(
1,
settings
.ptime_multiple
.nseconds()
.mul_div_ceil(clock_rate as u64, gst::ClockTime::SECOND.nseconds())
.unwrap() as u32,
);
gst::trace!(
CAT,
imp: self,
"min ptime {} (frames: {}), max ptime {} (frames: {}), ptime multiple {} (frames {})",
settings.min_ptime,
min_pframes,
settings.max_ptime.display(),
max_pframes.unwrap_or(0),
settings.ptime_multiple,
pframes_multiple,
);
let psize_multiple = pframes_multiple as usize * bpf;
let mut max_packet_size = self.obj().max_payload_size() as usize;
max_packet_size -= max_packet_size % psize_multiple;
if let Some(max_pframes) = max_pframes {
max_packet_size = cmp::min(max_pframes as usize * bpf, max_packet_size)
}
let mut min_packet_size = cmp::min(
cmp::max(min_pframes as usize * bpf, psize_multiple),
max_packet_size,
);
if let Some(ptime) = state.ptime {
let pframes = ptime
.nseconds()
.mul_div_ceil(clock_rate as u64, gst::ClockTime::SECOND.nseconds())
.unwrap() as u32;
let psize = pframes as usize * bpf;
min_packet_size = cmp::max(min_packet_size, psize);
max_packet_size = cmp::min(max_packet_size, psize);
}
(min_packet_size, max_packet_size, psize_multiple)
}
fn drain_packets(
&self,
settings: &Settings,
state: &mut State,
force: bool,
) -> Result<gst::FlowSuccess, gst::FlowError> {
// Always set when caps are set
let Some(clock_rate) = state.clock_rate else {
return Ok(gst::FlowSuccess::Ok);
};
let bpf = state.bpf.unwrap();
let (min_packet_size, max_packet_size, psize_multiple) =
self.calculate_packet_sizes(settings, state, clock_rate, bpf);
gst::trace!(
CAT,
imp: self,
"Currently {} bytes queued, min packet size {min_packet_size}, max packet size {max_packet_size}, force {force}",
state.queued_bytes,
);
while state.queued_bytes >= min_packet_size || (force && state.queued_bytes > 0) {
let packet_size = {
let mut packet_size = cmp::min(max_packet_size, state.queued_bytes);
packet_size -= packet_size % psize_multiple;
packet_size
};
gst::trace!(
CAT,
imp: self,
"Creating packet of size {packet_size} ({} frames), marker {}",
packet_size / bpf,
state.audio_discont.next_output_offset().is_none(),
);
// Set marker bit on the first packet after a discontinuity
let mut packet_builder = rtp_types::RtpPacketBuilder::new()
.marker_bit(state.audio_discont.next_output_offset().is_none());
let front = state.queued_buffers.front().unwrap();
let start_id = front.id;
let mut end_id = front.id;
// Fill payload from all relevant buffers and collect start/end ids that apply.
let mut remaining_packet_size = packet_size;
for buffer in state.queued_buffers.iter() {
let this_buffer_payload_size =
cmp::min(buffer.buffer.size() - buffer.offset, remaining_packet_size);
end_id = buffer.id;
packet_builder = packet_builder
.payload(&buffer.buffer[buffer.offset..][..this_buffer_payload_size]);
remaining_packet_size -= this_buffer_payload_size;
if remaining_packet_size == 0 {
break;
}
}
// Then create the packet.
self.obj().queue_packet(
PacketToBufferRelation::IdsWithOffset {
ids: start_id..=end_id,
timestamp_offset: {
if let Some(next_out_offset) = state.audio_discont.next_output_offset() {
TimestampOffset::Rtp(next_out_offset)
} else {
TimestampOffset::Pts(gst::ClockTime::ZERO)
}
},
},
packet_builder,
)?;
// And finally dequeue or update all currently queued buffers.
let mut remaining_packet_size = packet_size;
while remaining_packet_size > 0 {
let buffer = state.queued_buffers.front_mut().unwrap();
if buffer.buffer.size() - buffer.offset > remaining_packet_size {
buffer.offset += remaining_packet_size;
remaining_packet_size = 0;
} else {
remaining_packet_size -= buffer.buffer.size() - buffer.offset;
let _ = state.queued_buffers.pop_front();
}
}
state.queued_bytes -= packet_size;
state.audio_discont.process_output(packet_size / bpf);
}
gst::trace!(CAT, imp: self, "Currently {} bytes / {} frames queued", state.queued_bytes, state.queued_bytes / bpf);
Ok(gst::FlowSuccess::Ok)
}
}
/// Wrapper functions for public API.
#[allow(dead_code)]
impl RtpBaseAudioPay2 {
pub(super) fn set_bpf(&self, bpf: usize) {
let mut state = self.state.borrow_mut();
state.bpf = Some(bpf);
}
}

View file

@ -0,0 +1,40 @@
//
// Copyright (C) 2023 Sebastian Dröge <sebastian@centricular.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0
use gst::{glib, prelude::*, subclass::prelude::*};
use crate::basepay::RtpBasePay2Impl;
pub mod imp;
glib::wrapper! {
pub struct RtpBaseAudioPay2(ObjectSubclass<imp::RtpBaseAudioPay2>)
@extends crate::basepay::RtpBasePay2, gst::Element, gst::Object;
}
/// Trait containing extension methods for `RtpBaseAudioPay2`.
pub trait RtpBaseAudioPay2Ext: IsA<RtpBaseAudioPay2> {
/// Sets the number of bytes per frame.
///
/// Should always be called together with `RtpBasePay2Ext::set_src_caps()`.
fn set_bpf(&self, bpf: usize) {
self.upcast_ref::<RtpBaseAudioPay2>().imp().set_bpf(bpf)
}
}
impl<O: IsA<RtpBaseAudioPay2>> RtpBaseAudioPay2Ext for O {}
/// Trait to implement in `RtpBaseAudioPay2` subclasses.
pub trait RtpBaseAudioPay2Impl: RtpBasePay2Impl {}
unsafe impl<T: RtpBaseAudioPay2Impl> IsSubclassable<T> for RtpBaseAudioPay2 {
fn class_init(class: &mut glib::Class<Self>) {
Self::parent_class_init::<T>(class);
}
}

1937
net/rtp/src/basedepay/imp.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,543 @@
//
// Copyright (C) 2023 Sebastian Dröge <sebastian@centricular.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0
use std::ops::{Range, RangeBounds, RangeInclusive};
use gst::{glib, prelude::*, subclass::prelude::*};
pub mod imp;
glib::wrapper! {
pub struct RtpBaseDepay2(ObjectSubclass<imp::RtpBaseDepay2>)
@extends gst::Element, gst::Object;
}
/// Trait containing extension methods for `RtpBaseDepay2`.
pub trait RtpBaseDepay2Ext: IsA<RtpBaseDepay2> {
/// Sends a caps event with the given caps downstream before the next output buffer.
fn set_src_caps(&self, src_caps: &gst::Caps) {
assert!(src_caps.is_fixed());
self.upcast_ref::<RtpBaseDepay2>()
.imp()
.set_src_caps(src_caps);
}
/// Drop the packets of the given packet range.
///
/// This should be called when packets are dropped because they either don't make up any output
/// buffer or because they're corrupted in some way.
///
/// All pending packets up to the end of the range are dropped, i.e. the start of the range is
/// irrelevant.
///
/// It is not necessary to call this as part of `drain()` or `flush()` as all still pending packets are
/// considered dropped afterwards.
///
/// The next buffer that is finished will automatically get the `DISCONT` flag set.
// FIXME: Allow subclasses to drop explicit packet ranges while keeping older packets around to
// allow for continuing to reconstruct a frame despite broken packets in the middle.
fn drop_packets(&self, ext_seqnum: impl RangeBounds<u64>) {
self.upcast_ref::<RtpBaseDepay2>()
.imp()
.drop_packets(ext_seqnum)
}
/// Drop a single packet
///
/// Convenience wrapper for calling drop_packets() for a single packet.
fn drop_packet(&self, packet: &Packet) {
self.drop_packets(packet.ext_seqnum()..=packet.ext_seqnum())
}
/// Queue a buffer made from a given range of packet seqnums and timestamp offsets.
///
/// All buffers that are queued during one call of `handle_packet()` are collected in a
/// single buffer list and forwarded once `handle_packet()` has returned with a successful flow
/// return or `finish_pending_buffers()` was called.
///
/// All pending packets for which buffers were queued are released once `handle_packets()`
/// returned except for the last one. This means that it is possible for subclasses to queue a
/// buffer and output a remaining chunk of that buffer together with data from the next buffer.
///
/// If passing `OutOfBand` then the buffer is assumed to be produced using some other data,
/// e.g. from the caps, and not associated with any packets. In that case it will be pushed
/// right before the next buffer with the timestamp of that buffer, or at EOS with the
/// timestamp of the previous buffer.
///
/// In all other cases a seqnum range is provided and needs to be valid.
///
/// If the seqnum range doesn't start with the first pending packet then all packets up to the
/// first one given in the range are considered dropped.
///
/// Together with the seqnum range it is possible to provide a timestamp offset relative to
/// which the outgoing buffers's timestamp should be set.
///
/// The timestamp offset is provided in nanoseconds relative to the PTS of the packet that the
/// first seqnum refers to. This mode is mostly useful for subclasses that consume a packet
/// with multiple frames and send out one buffer per frame.
///
/// Both a PTS and DTS offset can be provided. If no DTS offset is provided then no DTS will be
/// set at all. If no PTS offset is provided then the first buffer for a given start seqnum
/// will get the PTS of the corresponding packet and all following buffers that start with the
/// same seqnum will get no PTS set.
///
/// Note that the DTS offset is relative to the final PTS and as such should not include the
/// PTS offset.
fn queue_buffer(
&self,
packet_to_buffer_relation: PacketToBufferRelation,
buffer: gst::Buffer,
) -> Result<gst::FlowSuccess, gst::FlowError> {
self.upcast_ref::<RtpBaseDepay2>()
.imp()
.queue_buffer(packet_to_buffer_relation, buffer)
}
/// Finish currently pending buffers and push them downstream in a single buffer list.
fn finish_pending_buffers(&self) -> Result<gst::FlowSuccess, gst::FlowError> {
self.upcast_ref::<RtpBaseDepay2>()
.imp()
.finish_pending_buffers()
}
/// Returns a reference to the sink pad.
fn sink_pad(&self) -> &gst::Pad {
self.upcast_ref::<RtpBaseDepay2>().imp().sink_pad()
}
/// Returns a reference to the src pad.
fn src_pad(&self) -> &gst::Pad {
self.upcast_ref::<RtpBaseDepay2>().imp().src_pad()
}
}
impl<O: IsA<RtpBaseDepay2>> RtpBaseDepay2Ext for O {}
/// Trait to implement in `RtpBaseDepay2` subclasses.
pub trait RtpBaseDepay2Impl: ElementImpl {
/// By default only metas without any tags are copied. Adding tags here will also copy the
/// metas that *only* have exactly one of these tags.
///
/// If more complex copying of metas is needed then [`RtpBaseDepay2Impl::transform_meta`] has
/// to be implemented.
const ALLOWED_META_TAGS: &'static [&'static str] = &[];
/// Called when streaming starts (READY -> PAUSED state change)
///
/// Optional, can be used to initialise streaming state.
fn start(&self) -> Result<(), gst::ErrorMessage> {
self.parent_start()
}
/// Called after streaming has stopped (PAUSED -> READY state change)
///
/// Optional, can be used to clean up streaming state.
fn stop(&self) -> Result<(), gst::ErrorMessage> {
self.parent_stop()
}
/// Called when new caps are received on the sink pad.
///
/// Can be used to configure the caps on the src pad or to configure caps-specific state.
/// If draining is necessary because of the caps change then the subclass will have to do that.
///
/// Optional, by default does nothing.
fn set_sink_caps(&self, caps: &gst::Caps) -> bool {
self.parent_set_sink_caps(caps)
}
/// Called whenever a new packet is available.
fn handle_packet(&self, packet: &Packet) -> Result<gst::FlowSuccess, gst::FlowError> {
self.parent_handle_packet(packet)
}
/// Called whenever a discontinuity or EOS is observed.
///
/// The subclass should output any pending buffers it can output at this point.
///
/// This will be followed by a call to [`Self::flush`].
///
/// Optional, by default drops all still pending packets and forwards all still pending buffers
/// with the last known timestamp.
fn drain(&self) -> Result<gst::FlowSuccess, gst::FlowError> {
self.parent_drain()
}
/// Called on `FlushStop` or whenever all pending data should simply be discarded.
///
/// The subclass should reset its internal state as necessary.
///
/// Optional.
fn flush(&self) {
self.parent_flush()
}
/// Called whenever a new event arrives on the sink pad.
///
/// Optional, by default does the standard event handling of the base class.
fn sink_event(&self, event: gst::Event) -> Result<gst::FlowSuccess, gst::FlowError> {
self.parent_sink_event(event)
}
/// Called whenever a new event arrives on the src pad.
///
/// Optional, by default does the standard event handling of the base class.
fn src_event(&self, event: gst::Event) -> Result<gst::FlowSuccess, gst::FlowError> {
self.parent_src_event(event)
}
/// Called whenever a new query arrives on the sink pad.
///
/// Optional, by default does the standard query handling of the base class.
fn sink_query(&self, query: &mut gst::QueryRef) -> bool {
self.parent_sink_query(query)
}
/// Called whenever a new query arrives on the src pad.
///
/// Optional, by default does the standard query handling of the base class.
fn src_query(&self, query: &mut gst::QueryRef) -> bool {
self.parent_src_query(query)
}
/// Called whenever a meta from an input buffer has to be copied to the output buffer.
///
/// Optional, by default simply copies over all metas.
fn transform_meta(
&self,
in_buf: &gst::BufferRef,
meta: &gst::MetaRef<gst::Meta>,
out_buf: &mut gst::BufferRef,
) {
self.parent_transform_meta(in_buf, meta, out_buf);
}
}
/// Trait containing extension methods for `RtpBaseDepay2Impl`, specifically methods for chaining
/// up to the parent implementation of virtual methods.
pub trait RtpBaseDepay2ImplExt: RtpBaseDepay2Impl {
fn parent_set_sink_caps(&self, caps: &gst::Caps) -> bool {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.set_sink_caps)(self.obj().unsafe_cast_ref(), caps)
}
}
fn parent_start(&self) -> Result<(), gst::ErrorMessage> {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.start)(self.obj().unsafe_cast_ref())
}
}
fn parent_stop(&self) -> Result<(), gst::ErrorMessage> {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.stop)(self.obj().unsafe_cast_ref())
}
}
fn parent_handle_packet(&self, packet: &Packet) -> Result<gst::FlowSuccess, gst::FlowError> {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.handle_packet)(self.obj().unsafe_cast_ref(), packet)
}
}
fn parent_drain(&self) -> Result<gst::FlowSuccess, gst::FlowError> {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.drain)(self.obj().unsafe_cast_ref())
}
}
fn parent_flush(&self) {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.flush)(self.obj().unsafe_cast_ref());
}
}
fn parent_sink_event(&self, event: gst::Event) -> Result<gst::FlowSuccess, gst::FlowError> {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.sink_event)(self.obj().unsafe_cast_ref(), event)
}
}
fn parent_src_event(&self, event: gst::Event) -> Result<gst::FlowSuccess, gst::FlowError> {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.src_event)(self.obj().unsafe_cast_ref(), event)
}
}
fn parent_sink_query(&self, query: &mut gst::QueryRef) -> bool {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.sink_query)(self.obj().unsafe_cast_ref(), query)
}
}
fn parent_src_query(&self, query: &mut gst::QueryRef) -> bool {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.src_query)(self.obj().unsafe_cast_ref(), query)
}
}
fn parent_transform_meta(
&self,
in_buf: &gst::BufferRef,
meta: &gst::MetaRef<gst::Meta>,
out_buf: &mut gst::BufferRef,
) {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.transform_meta)(self.obj().unsafe_cast_ref(), in_buf, meta, out_buf)
}
}
}
impl<T: RtpBaseDepay2Impl> RtpBaseDepay2ImplExt for T {}
#[derive(Debug)]
pub struct Packet {
buffer: gst::MappedBuffer<gst::buffer::Readable>,
discont: bool,
ext_seqnum: u64,
ext_timestamp: u64,
marker: bool,
payload_range: Range<usize>,
}
impl Packet {
pub fn ext_seqnum(&self) -> u64 {
self.ext_seqnum
}
pub fn ext_timestamp(&self) -> u64 {
self.ext_timestamp
}
pub fn marker_bit(&self) -> bool {
self.marker
}
pub fn discont(&self) -> bool {
self.discont
}
pub fn pts(&self) -> Option<gst::ClockTime> {
self.buffer.buffer().pts()
}
pub fn payload(&self) -> &[u8] {
&self.buffer[self.payload_range.clone()]
}
pub fn payload_buffer(&self) -> gst::Buffer {
self.buffer
.buffer()
.copy_region(
gst::BufferCopyFlags::MEMORY,
self.payload_range.start..self.payload_range.end,
)
.expect("Failed copying buffer")
}
/// Note: This function will panic if the range is out of bounds.
pub fn payload_subbuffer(&self, range: impl RangeBounds<usize>) -> gst::Buffer {
let range_start = match range.start_bound() {
std::ops::Bound::Included(&start) => start,
std::ops::Bound::Excluded(&start) => start.checked_add(1).unwrap(),
std::ops::Bound::Unbounded => 0,
}
.checked_add(self.payload_range.start)
.unwrap();
let range_end = match range.end_bound() {
std::ops::Bound::Included(&range_end) => range_end
.checked_add(self.payload_range.start)
.and_then(|v| v.checked_add(1))
.unwrap(),
std::ops::Bound::Excluded(&range_end) => {
range_end.checked_add(self.payload_range.start).unwrap()
}
std::ops::Bound::Unbounded => self.payload_range.end,
};
self.buffer
.buffer()
.copy_region(gst::BufferCopyFlags::MEMORY, range_start..range_end)
.expect("Failed to create subbuffer")
}
/// Note: For offset with unspecified length just use `payload_subbuffer(off..)`.
/// Note: This function will panic if the offset or length are out of bounds.
pub fn payload_subbuffer_from_offset_with_length(
&self,
start: usize,
length: usize,
) -> gst::Buffer {
self.payload_subbuffer(start..start + length)
}
}
/// Class struct for `RtpBaseDepay2`.
#[repr(C)]
pub struct Class {
parent: gst::ffi::GstElementClass,
start: fn(&RtpBaseDepay2) -> Result<(), gst::ErrorMessage>,
stop: fn(&RtpBaseDepay2) -> Result<(), gst::ErrorMessage>,
set_sink_caps: fn(&RtpBaseDepay2, caps: &gst::Caps) -> bool,
handle_packet: fn(&RtpBaseDepay2, packet: &Packet) -> Result<gst::FlowSuccess, gst::FlowError>,
drain: fn(&RtpBaseDepay2) -> Result<gst::FlowSuccess, gst::FlowError>,
flush: fn(&RtpBaseDepay2),
sink_event: fn(&RtpBaseDepay2, event: gst::Event) -> Result<gst::FlowSuccess, gst::FlowError>,
src_event: fn(&RtpBaseDepay2, event: gst::Event) -> Result<gst::FlowSuccess, gst::FlowError>,
sink_query: fn(&RtpBaseDepay2, query: &mut gst::QueryRef) -> bool,
src_query: fn(&RtpBaseDepay2, query: &mut gst::QueryRef) -> bool,
transform_meta: fn(
&RtpBaseDepay2,
in_buf: &gst::BufferRef,
meta: &gst::MetaRef<gst::Meta>,
out_buf: &mut gst::BufferRef,
),
allowed_meta_tags: &'static [&'static str],
}
unsafe impl ClassStruct for Class {
type Type = imp::RtpBaseDepay2;
}
impl std::ops::Deref for Class {
type Target = glib::Class<<<Self as ClassStruct>::Type as ObjectSubclass>::ParentType>;
fn deref(&self) -> &Self::Target {
unsafe { &*(&self.parent as *const _ as *const _) }
}
}
unsafe impl<T: RtpBaseDepay2Impl> IsSubclassable<T> for RtpBaseDepay2 {
fn class_init(class: &mut glib::Class<Self>) {
Self::parent_class_init::<T>(class);
let class = class.as_mut();
class.start = |obj| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.start()
};
class.stop = |obj| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.stop()
};
class.set_sink_caps = |obj, caps| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.set_sink_caps(caps)
};
class.handle_packet = |obj, packet| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.handle_packet(packet)
};
class.drain = |obj| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.drain()
};
class.flush = |obj| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.flush()
};
class.sink_event = |obj, event| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.sink_event(event)
};
class.src_event = |obj, event| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.src_event(event)
};
class.sink_query = |obj, query| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.sink_query(query)
};
class.src_query = |obj, query| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.src_query(query)
};
class.transform_meta = |obj, in_buf, meta, out_buf| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.transform_meta(in_buf, meta, out_buf)
};
class.allowed_meta_tags = T::ALLOWED_META_TAGS;
}
}
/// Timestamp offset between this buffer and the reference packet.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum TimestampOffset {
/// Offset in nanoseconds relative to the first seqnum this buffer belongs to.
Pts(gst::Signed<gst::ClockTime>),
/// Offset in nanoseconds relative to the first seqnum this buffer belongs to.
PtsAndDts(gst::Signed<gst::ClockTime>, gst::Signed<gst::ClockTime>),
}
/// Relation between queued buffer and input packet seqnums.
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub enum PacketToBufferRelation {
Seqnums(RangeInclusive<u64>),
SeqnumsWithOffset {
seqnums: RangeInclusive<u64>,
timestamp_offset: TimestampOffset,
},
OutOfBand,
}
impl<'a> From<&'a Packet> for PacketToBufferRelation {
fn from(packet: &'a Packet) -> Self {
PacketToBufferRelation::Seqnums(packet.ext_seqnum()..=packet.ext_seqnum())
}
}
impl From<u64> for PacketToBufferRelation {
fn from(ext_seqnum: u64) -> Self {
PacketToBufferRelation::Seqnums(ext_seqnum..=ext_seqnum)
}
}

2115
net/rtp/src/basepay/imp.rs Normal file

File diff suppressed because it is too large Load diff

494
net/rtp/src/basepay/mod.rs Normal file
View file

@ -0,0 +1,494 @@
//
// Copyright (C) 2023 Sebastian Dröge <sebastian@centricular.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0
use std::ops::{RangeBounds, RangeInclusive};
use gst::{glib, prelude::*, subclass::prelude::*};
pub mod imp;
glib::wrapper! {
pub struct RtpBasePay2(ObjectSubclass<imp::RtpBasePay2>)
@extends gst::Element, gst::Object;
}
/// Trait containing extension methods for `RtpBasePay2`.
pub trait RtpBasePay2Ext: IsA<RtpBasePay2> {
/// Sends a caps event with the given caps downstream before the next output buffer.
///
/// The caps must be `application/x-rtp` and contain the `clock-rate` field with a suitable
/// clock-rate for this stream.
///
/// The caps can be unfixed and will be passed through `RtpBasePay2Impl::negotiate()` to
/// negotiate caps with downstream, and finally fixate them.
fn set_src_caps(&self, src_caps: &gst::Caps) {
self.upcast_ref::<RtpBasePay2>()
.imp()
.set_src_caps(src_caps);
}
/// Drop the buffers from the given buffer range.
///
/// This should be called when input buffers are dropped because they are not included in any
/// output packet.
///
/// All pending buffers up to the end of the range are dropped, i.e. the start of the range is
/// irrelevant.
fn drop_buffers(&self, ids: impl RangeBounds<u64>) {
self.upcast_ref::<RtpBasePay2>().imp().drop_buffers(ids)
}
/// Queue an RTP packet made from a given range of input buffer ids and timestamp offset.
///
/// All packets that are queued during one call of `handle_buffer()` are collected in a
/// single buffer list and forwarded once `handle_buffer()` has returned with a successful flow
/// return or `finish_pending_packets()` was called.
///
/// All pending buffers for which packets were queued are released once `handle_buffer()`
/// returned except for the last one. This means that it is possible for subclasses to queue a
/// buffer and output a remaining chunk of that buffer together with data from the next buffer.
///
/// If passing `OutOfBand` then the packet is assumed to be produced using some other data,
/// e.g. from the caps, and not associated with any packets. In that case it will be pushed
/// right before the next packet with the timestamp of that packet, or at EOS with the
/// timestamp of the previous packet.
///
/// In all other cases a buffer id range is provided and needs to be valid.
///
/// If the id range doesn't start with the first pending buffer then all buffers up to the
/// first one given in the range are considered dropped.
///
/// Together with the buffer ids it is possible to provide a timestamp offset relative to which
/// the outgoing RTP timestamp and GStreamer PTS should be set.
///
/// The timestamp offset can be provided in two ways:
///
/// * Nanoseconds relative to the PTS of the buffer that the first id refers to. This mode is
/// mostly useful for subclasses that consume a buffer with multiple frames and send out
/// one packet per frame.
///
/// * RTP clock-rate units (without wrap-arounds) relative to the last buffer that had no
/// timestamp offset given in RTP clock-rate units. In this mode the subclass needs to be
/// careful to handle discontinuities of any sort correctly. Also, the subclass needs to
/// provide an explicit timestamp (either by setting no offset or by setting a PTS-based
/// offset) for the first packet ever and after every `drain()` or `flush()`.
fn queue_packet(
&self,
packet_to_buffer_relation: PacketToBufferRelation,
packet: rtp_types::RtpPacketBuilder<&[u8], &[u8]>,
) -> Result<gst::FlowSuccess, gst::FlowError> {
self.upcast_ref::<RtpBasePay2>()
.imp()
.queue_packet(packet_to_buffer_relation, packet)
}
/// Finish currently pending packets and push them downstream in a single buffer list.
fn finish_pending_packets(&self) -> Result<gst::FlowSuccess, gst::FlowError> {
self.upcast_ref::<RtpBasePay2>()
.imp()
.finish_pending_packets()
}
/// Returns a reference to the sink pad.
fn sink_pad(&self) -> &gst::Pad {
self.upcast_ref::<RtpBasePay2>().imp().sink_pad()
}
/// Returns a reference to the src pad.
fn src_pad(&self) -> &gst::Pad {
self.upcast_ref::<RtpBasePay2>().imp().src_pad()
}
/// Returns the currently configured MTU.
fn mtu(&self) -> u32 {
self.upcast_ref::<RtpBasePay2>().imp().mtu()
}
/// Returns the maximum available payload size.
fn max_payload_size(&self) -> u32 {
self.upcast_ref::<RtpBasePay2>().imp().max_payload_size()
}
}
impl<O: IsA<RtpBasePay2>> RtpBasePay2Ext for O {}
/// Trait to implement in `RtpBasePay2` subclasses.
pub trait RtpBasePay2Impl: ElementImpl {
/// Drop buffers with `HEADER` flag.
const DROP_HEADER_BUFFERS: bool = false;
/// By default only metas without any tags are copied. Adding tags here will also copy the
/// metas that *only* have exactly one of these tags.
///
/// If more complex copying of metas is needed then [`RtpBasePay2Impl::transform_meta`] has
/// to be implemented.
const ALLOWED_META_TAGS: &'static [&'static str] = &[];
/// Called when streaming starts (READY -> PAUSED state change)
///
/// Optional, can be used to initialise streaming state.
fn start(&self) -> Result<(), gst::ErrorMessage> {
self.parent_start()
}
/// Called after streaming has stopped (PAUSED -> READY state change)
///
/// Optional, can be used to clean up streaming state.
fn stop(&self) -> Result<(), gst::ErrorMessage> {
self.parent_stop()
}
/// Called when new caps are received on the sink pad.
///
/// Can be used to configure the caps on the src pad or to configure caps-specific state.
///
/// Optional, by default does nothing.
fn set_sink_caps(&self, caps: &gst::Caps) -> bool {
self.parent_set_sink_caps(caps)
}
/// Called when new caps are configured on the source pad and whenever renegotiation has to happen.
///
/// The `src_caps` are the caps passed into `set_src_caps()` before, intersected with the
/// supported caps by the peer, and will have to be fixated.
///
/// Optional, by default sets the `payload` (pt) and `ssrc` fields, and negotiates RTP header
/// extensions with downstream, and finally fixates the caps and configures them on the source
/// pad.
fn negotiate(&self, src_caps: gst::Caps) {
assert!(src_caps.is_writable());
self.parent_negotiate(src_caps);
}
/// Called whenever a new buffer is available.
fn handle_buffer(
&self,
buffer: &gst::Buffer,
id: u64,
) -> Result<gst::FlowSuccess, gst::FlowError> {
self.parent_handle_buffer(buffer, id)
}
/// Called whenever a discontinuity or EOS is observed.
///
/// The subclass should output any pending buffers it can output at this point.
///
/// This will be followed by a call to [`Self::flush`].
///
/// Optional, by default drops all still pending buffers and forwards all still pending packets
/// with the last known timestamp.
fn drain(&self) -> Result<gst::FlowSuccess, gst::FlowError> {
self.parent_drain()
}
/// Called on `FlushStop` or whenever all pending data should simply be discarded.
///
/// The subclass should reset its internal state as necessary.
///
/// Optional.
fn flush(&self) {
self.parent_flush()
}
/// Called whenever a new event arrives on the sink pad.
///
/// Optional, by default does the standard event handling of the base class.
fn sink_event(&self, event: gst::Event) -> Result<gst::FlowSuccess, gst::FlowError> {
self.parent_sink_event(event)
}
/// Called whenever a new event arrives on the src pad.
///
/// Optional, by default does the standard event handling of the base class.
fn src_event(&self, event: gst::Event) -> Result<gst::FlowSuccess, gst::FlowError> {
self.parent_src_event(event)
}
/// Called whenever a new query arrives on the sink pad.
///
/// Optional, by default does the standard query handling of the base class.
fn sink_query(&self, query: &mut gst::QueryRef) -> bool {
self.parent_sink_query(query)
}
/// Called whenever a new query arrives on the src pad.
///
/// Optional, by default does the standard query handling of the base class.
fn src_query(&self, query: &mut gst::QueryRef) -> bool {
self.parent_src_query(query)
}
/// Called whenever a meta from an input buffer has to be copied to the output buffer.
///
/// Optional, by default simply copies over all metas.
fn transform_meta(
&self,
in_buf: &gst::BufferRef,
meta: &gst::MetaRef<gst::Meta>,
out_buf: &mut gst::BufferRef,
) {
self.parent_transform_meta(in_buf, meta, out_buf);
}
}
/// Trait containing extension methods for `RtpBasePay2Impl`, specifically methods for chaining
/// up to the parent implementation of virtual methods.
pub trait RtpBasePay2ImplExt: RtpBasePay2Impl {
fn parent_set_sink_caps(&self, caps: &gst::Caps) -> bool {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.set_sink_caps)(self.obj().unsafe_cast_ref(), caps)
}
}
fn parent_negotiate(&self, src_caps: gst::Caps) {
assert!(src_caps.is_writable());
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.negotiate)(self.obj().unsafe_cast_ref(), src_caps);
}
}
fn parent_start(&self) -> Result<(), gst::ErrorMessage> {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.start)(self.obj().unsafe_cast_ref())
}
}
fn parent_stop(&self) -> Result<(), gst::ErrorMessage> {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.stop)(self.obj().unsafe_cast_ref())
}
}
fn parent_handle_buffer(
&self,
buffer: &gst::Buffer,
id: u64,
) -> Result<gst::FlowSuccess, gst::FlowError> {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.handle_buffer)(self.obj().unsafe_cast_ref(), buffer, id)
}
}
fn parent_drain(&self) -> Result<gst::FlowSuccess, gst::FlowError> {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.drain)(self.obj().unsafe_cast_ref())
}
}
fn parent_flush(&self) {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.flush)(self.obj().unsafe_cast_ref());
}
}
fn parent_sink_event(&self, event: gst::Event) -> Result<gst::FlowSuccess, gst::FlowError> {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.sink_event)(self.obj().unsafe_cast_ref(), event)
}
}
fn parent_src_event(&self, event: gst::Event) -> Result<gst::FlowSuccess, gst::FlowError> {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.src_event)(self.obj().unsafe_cast_ref(), event)
}
}
fn parent_sink_query(&self, query: &mut gst::QueryRef) -> bool {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.sink_query)(self.obj().unsafe_cast_ref(), query)
}
}
fn parent_src_query(&self, query: &mut gst::QueryRef) -> bool {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.src_query)(self.obj().unsafe_cast_ref(), query)
}
}
fn parent_transform_meta(
&self,
in_buf: &gst::BufferRef,
meta: &gst::MetaRef<gst::Meta>,
out_buf: &mut gst::BufferRef,
) {
unsafe {
let data = Self::type_data();
let parent_class = &*(data.as_ref().parent_class() as *mut Class);
(parent_class.transform_meta)(self.obj().unsafe_cast_ref(), in_buf, meta, out_buf)
}
}
}
impl<T: RtpBasePay2Impl> RtpBasePay2ImplExt for T {}
/// Class struct for `RtpBasePay2`.
#[repr(C)]
pub struct Class {
parent: gst::ffi::GstElementClass,
start: fn(&RtpBasePay2) -> Result<(), gst::ErrorMessage>,
stop: fn(&RtpBasePay2) -> Result<(), gst::ErrorMessage>,
set_sink_caps: fn(&RtpBasePay2, caps: &gst::Caps) -> bool,
negotiate: fn(&RtpBasePay2, src_caps: gst::Caps),
handle_buffer:
fn(&RtpBasePay2, buffer: &gst::Buffer, id: u64) -> Result<gst::FlowSuccess, gst::FlowError>,
drain: fn(&RtpBasePay2) -> Result<gst::FlowSuccess, gst::FlowError>,
flush: fn(&RtpBasePay2),
sink_event: fn(&RtpBasePay2, event: gst::Event) -> Result<gst::FlowSuccess, gst::FlowError>,
src_event: fn(&RtpBasePay2, event: gst::Event) -> Result<gst::FlowSuccess, gst::FlowError>,
sink_query: fn(&RtpBasePay2, query: &mut gst::QueryRef) -> bool,
src_query: fn(&RtpBasePay2, query: &mut gst::QueryRef) -> bool,
transform_meta: fn(
&RtpBasePay2,
in_buf: &gst::BufferRef,
meta: &gst::MetaRef<gst::Meta>,
out_buf: &mut gst::BufferRef,
),
allowed_meta_tags: &'static [&'static str],
drop_header_buffers: bool,
}
unsafe impl ClassStruct for Class {
type Type = imp::RtpBasePay2;
}
impl std::ops::Deref for Class {
type Target = glib::Class<<<Self as ClassStruct>::Type as ObjectSubclass>::ParentType>;
fn deref(&self) -> &Self::Target {
unsafe { &*(&self.parent as *const _ as *const _) }
}
}
unsafe impl<T: RtpBasePay2Impl> IsSubclassable<T> for RtpBasePay2 {
fn class_init(class: &mut glib::Class<Self>) {
Self::parent_class_init::<T>(class);
let class = class.as_mut();
class.start = |obj| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.start()
};
class.stop = |obj| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.stop()
};
class.set_sink_caps = |obj, caps| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.set_sink_caps(caps)
};
class.negotiate = |obj, src_caps| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.negotiate(src_caps)
};
class.handle_buffer = |obj, buffer, id| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.handle_buffer(buffer, id)
};
class.drain = |obj| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.drain()
};
class.flush = |obj| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.flush()
};
class.sink_event = |obj, event| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.sink_event(event)
};
class.src_event = |obj, event| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.src_event(event)
};
class.sink_query = |obj, query| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.sink_query(query)
};
class.src_query = |obj, query| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.src_query(query)
};
class.transform_meta = |obj, in_buf, meta, out_buf| unsafe {
let imp = obj.unsafe_cast_ref::<T::Type>().imp();
imp.transform_meta(in_buf, meta, out_buf)
};
class.allowed_meta_tags = T::ALLOWED_META_TAGS;
class.drop_header_buffers = T::DROP_HEADER_BUFFERS;
}
}
/// Timestamp offset between this packet and the reference.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum TimestampOffset {
/// Offset in nanoseconds relative to the first buffer id this packet belongs to.
Pts(gst::ClockTime),
/// Offset in RTP clock-time units relative to the last packet that had offset given in RTP
/// clock-rate units.
Rtp(u64),
}
/// Relation between queued packet and input buffer ids.
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub enum PacketToBufferRelation {
Ids(RangeInclusive<u64>),
IdsWithOffset {
ids: RangeInclusive<u64>,
timestamp_offset: TimestampOffset,
},
OutOfBand,
}
impl From<u64> for PacketToBufferRelation {
fn from(id: u64) -> Self {
PacketToBufferRelation::Ids(id..=id)
}
}

View file

@ -1,5 +1,6 @@
//
// Copyright (C) 2022 Vivienne Watermeier <vwatermeier@igalia.com>
// Copyright (C) 2022-24 Sebastian Dröge <sebastian@centricular.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
@ -18,11 +19,36 @@ use gst::glib;
mod av1;
mod gcc;
mod audio_discont;
mod baseaudiopay;
mod basedepay;
mod basepay;
mod pcmau;
#[cfg(test)]
mod tests;
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
av1::depay::register(plugin)?;
av1::pay::register(plugin)?;
gcc::register(plugin)?;
#[cfg(feature = "doc")]
{
use gst::prelude::*;
crate::basepay::RtpBasePay2::static_type() // make base classes available in docs
.mark_as_plugin_api(gst::PluginAPIFlags::empty());
crate::basedepay::RtpBaseDepay2::static_type()
.mark_as_plugin_api(gst::PluginAPIFlags::empty());
crate::baseaudiopay::RtpBaseAudioPay2::static_type()
.mark_as_plugin_api(gst::PluginAPIFlags::empty());
}
pcmau::depay::register(plugin)?;
pcmau::pay::register(plugin)?;
Ok(())
}

View file

@ -0,0 +1,286 @@
//
// Copyright (C) 2023 Sebastian Dröge <sebastian@centricular.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0
use atomic_refcell::AtomicRefCell;
use gst::{glib, prelude::*, subclass::prelude::*};
use once_cell::sync::Lazy;
use crate::basedepay::RtpBaseDepay2Ext;
#[derive(Default)]
pub struct RtpPcmauDepay {
state: AtomicRefCell<State>,
}
#[derive(Default)]
struct State {
clock_rate: Option<u32>,
}
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
gst::DebugCategory::new(
"rtppcmaudepay2",
gst::DebugColorFlags::empty(),
Some("RTP PCMA/PCMU Depayloader"),
)
});
#[glib::object_subclass]
impl ObjectSubclass for RtpPcmauDepay {
const NAME: &'static str = "GstRtpPcmauDepay2";
type Type = super::RtpPcmauDepay;
type ParentType = crate::basedepay::RtpBaseDepay2;
}
impl ObjectImpl for RtpPcmauDepay {}
impl GstObjectImpl for RtpPcmauDepay {}
impl ElementImpl for RtpPcmauDepay {}
impl crate::basedepay::RtpBaseDepay2Impl for RtpPcmauDepay {
const ALLOWED_META_TAGS: &'static [&'static str] = &["audio"];
fn set_sink_caps(&self, caps: &gst::Caps) -> bool {
let s = caps.structure(0).unwrap();
let clock_rate = s.get::<i32>("clock-rate").unwrap_or(8000);
let src_caps = gst::Caps::builder(
if self.obj().type_() == super::RtpPcmaDepay::static_type() {
"audio/x-alaw"
} else {
"audio/x-mulaw"
},
)
.field("channels", 1i32)
.field("rate", clock_rate)
.build();
self.state.borrow_mut().clock_rate = Some(clock_rate as u32);
self.obj().set_src_caps(&src_caps);
true
}
fn handle_packet(
&self,
packet: &crate::basedepay::Packet,
) -> Result<gst::FlowSuccess, gst::FlowError> {
let mut buffer = packet.payload_buffer();
let state = self.state.borrow();
// Always set when caps are set
let clock_rate = state.clock_rate.unwrap();
let buffer_ref = buffer.get_mut().unwrap();
buffer_ref.set_duration(
(buffer_ref.size() as u64)
.mul_div_floor(*gst::ClockTime::SECOND, clock_rate as u64)
.map(gst::ClockTime::from_nseconds),
);
// mark start of talkspurt with RESYNC
if packet.marker_bit() {
buffer_ref.set_flags(gst::BufferFlags::RESYNC);
}
gst::trace!(CAT, imp: self, "Finishing buffer {buffer:?}");
self.obj().queue_buffer(packet.into(), buffer)
}
}
/**
* SECTION:element-rtppcmadepay2
* @see_also: rtppcmapay2, rtppcmupay2, rtppcmudepay2, alawenc, alawdec
*
* Extracts A-law encoded audio from RTP packets as per [RFC 3551][rfc-3551].
*
* [rfc-3551]: https://www.rfc-editor.org/rfc/rfc3551.html#section-4.5.14
*
* ## Example pipeline
*
* |[
* gst-launch-1.0 udpsrc caps='application/x-rtp, media=audio, clock-rate=8000, payload=8' ! rtpjitterbuffer latency=50 ! rtppcmadepay2 ! alawdec ! audioconvert ! audioresample ! autoaudiosink
* ]| This will depayload an incoming RTP A-law audio stream. You can use the #rtppcmapay2 and
* alawenc elements to create such an RTP stream.
*
* Since: plugins-rs-0.13.0
*/
#[derive(Default)]
pub struct RtpPcmaDepay;
#[glib::object_subclass]
impl ObjectSubclass for RtpPcmaDepay {
const NAME: &'static str = "GstRtpPcmaDepay2";
type Type = super::RtpPcmaDepay;
type ParentType = super::RtpPcmauDepay;
}
impl ObjectImpl for RtpPcmaDepay {}
impl GstObjectImpl for RtpPcmaDepay {}
impl ElementImpl for RtpPcmaDepay {
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
gst::subclass::ElementMetadata::new(
"RTP PCMA Depayloader",
"Codec/Depayloader/Network/RTP",
"Depayload A-law from RTP packets (RFC 3551)",
"Sebastian Dröge <sebastian@centricular.com>",
)
});
Some(&*ELEMENT_METADATA)
}
fn pad_templates() -> &'static [gst::PadTemplate] {
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
let sink_pad_template = gst::PadTemplate::new(
"sink",
gst::PadDirection::Sink,
gst::PadPresence::Always,
&gst::Caps::builder_full()
.structure(
gst::Structure::builder("application/x-rtp")
.field("media", "audio")
.field("payload", 8i32)
.field("clock-rate", 8000i32)
.build(),
)
.structure(
gst::Structure::builder("application/x-rtp")
.field("media", "audio")
.field("clock-rate", gst::IntRange::new(1i32, i32::MAX))
.field("encoding-name", "PCMA")
.build(),
)
.build(),
)
.unwrap();
let src_pad_template = gst::PadTemplate::new(
"src",
gst::PadDirection::Src,
gst::PadPresence::Always,
&gst::Caps::builder("audio/x-alaw")
.field("channels", 1i32)
.field("rate", gst::IntRange::new(1i32, i32::MAX))
.build(),
)
.unwrap();
vec![src_pad_template, sink_pad_template]
});
PAD_TEMPLATES.as_ref()
}
}
impl crate::basedepay::RtpBaseDepay2Impl for RtpPcmaDepay {}
impl super::RtpPcmauDepayImpl for RtpPcmaDepay {}
/**
* SECTION:element-rtppcmudepay2
* @see_also: rtppcmupay2, rtppcmapay2, rtppcmadepay2, mulawenc, mulawdec
*
* Extracts µ-law encoded audio from RTP packets as per [RFC 3551][rfc-3551].
*
* [rfc-3551]: https://www.rfc-editor.org/rfc/rfc3551.html#section-4.5.14
*
* ## Example pipeline
*
* |[
* gst-launch-1.0 udpsrc caps='application/x-rtp, media=audio, clock-rate=8000, payload=0' ! rtpjitterbuffer latency=50 ! rtppcmudepay2 ! mulawdec ! audioconvert ! audioresample ! autoaudiosink
* ]| This will depayload an incoming RTP µ-law audio stream. You can use the #rtppcmupay2 and
* mulawenc elements to create such an RTP stream.
*
* Since: plugins-rs-0.13.0
*/
#[derive(Default)]
pub struct RtpPcmuDepay;
#[glib::object_subclass]
impl ObjectSubclass for RtpPcmuDepay {
const NAME: &'static str = "GstRtpPcmuDepay2";
type Type = super::RtpPcmuDepay;
type ParentType = super::RtpPcmauDepay;
}
impl ObjectImpl for RtpPcmuDepay {}
impl GstObjectImpl for RtpPcmuDepay {}
impl ElementImpl for RtpPcmuDepay {
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
gst::subclass::ElementMetadata::new(
"RTP PCMU Depayloader",
"Codec/Depayloader/Network/RTP",
"Depayload µ-law from RTP packets (RFC 3551)",
"Sebastian Dröge <sebastian@centricular.com>",
)
});
Some(&*ELEMENT_METADATA)
}
fn pad_templates() -> &'static [gst::PadTemplate] {
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
let sink_pad_template = gst::PadTemplate::new(
"sink",
gst::PadDirection::Sink,
gst::PadPresence::Always,
&gst::Caps::builder_full()
.structure(
gst::Structure::builder("application/x-rtp")
.field("media", "audio")
.field("payload", 0i32)
.field("clock-rate", 8000i32)
.build(),
)
.structure(
gst::Structure::builder("application/x-rtp")
.field("media", "audio")
.field("clock-rate", gst::IntRange::new(1i32, i32::MAX))
.field("encoding-name", "PCMU")
.build(),
)
.build(),
)
.unwrap();
let src_pad_template = gst::PadTemplate::new(
"src",
gst::PadDirection::Src,
gst::PadPresence::Always,
&gst::Caps::builder("audio/x-mulaw")
.field("channels", 1i32)
.field("rate", gst::IntRange::new(1i32, i32::MAX))
.build(),
)
.unwrap();
vec![src_pad_template, sink_pad_template]
});
PAD_TEMPLATES.as_ref()
}
}
impl crate::basedepay::RtpBaseDepay2Impl for RtpPcmuDepay {}
impl super::RtpPcmauDepayImpl for RtpPcmuDepay {}

View file

@ -0,0 +1,59 @@
//
// Copyright (C) 2023 Sebastian Dröge <sebastian@centricular.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0
use gst::{glib, prelude::*, subclass::prelude::*};
pub mod imp;
glib::wrapper! {
pub struct RtpPcmauDepay(ObjectSubclass<imp::RtpPcmauDepay>)
@extends crate::basedepay::RtpBaseDepay2, gst::Element, gst::Object;
}
pub trait RtpPcmauDepayImpl: crate::basedepay::RtpBaseDepay2Impl {}
unsafe impl<T: RtpPcmauDepayImpl> IsSubclassable<T> for RtpPcmauDepay {
fn class_init(class: &mut glib::Class<Self>) {
Self::parent_class_init::<T>(class);
}
}
glib::wrapper! {
pub struct RtpPcmaDepay(ObjectSubclass<imp::RtpPcmaDepay>)
@extends RtpPcmauDepay, crate::basedepay::RtpBaseDepay2, gst::Element, gst::Object;
}
glib::wrapper! {
pub struct RtpPcmuDepay(ObjectSubclass<imp::RtpPcmuDepay>)
@extends RtpPcmauDepay, crate::basedepay::RtpBaseDepay2, gst::Element, gst::Object;
}
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
#[cfg(feature = "doc")]
{
use gst::prelude::*;
// Make internal base class available in docs
crate::pcmau::depay::RtpPcmauDepay::static_type()
.mark_as_plugin_api(gst::PluginAPIFlags::empty());
}
gst::Element::register(
Some(plugin),
"rtppcmadepay2",
gst::Rank::MARGINAL,
RtpPcmaDepay::static_type(),
)?;
gst::Element::register(
Some(plugin),
"rtppcmudepay2",
gst::Rank::MARGINAL,
RtpPcmuDepay::static_type(),
)
}

14
net/rtp/src/pcmau/mod.rs Normal file
View file

@ -0,0 +1,14 @@
//
// Copyright (C) 2023 Sebastian Dröge <sebastian@centricular.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0
pub mod depay;
pub mod pay;
#[cfg(test)]
mod tests;

View file

@ -0,0 +1,294 @@
//
// Copyright (C) 2023 Sebastian Dröge <sebastian@centricular.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0
use gst::{glib, prelude::*, subclass::prelude::*};
use once_cell::sync::Lazy;
use crate::{
baseaudiopay::RtpBaseAudioPay2Ext,
basepay::{RtpBasePay2Ext, RtpBasePay2ImplExt},
};
#[derive(Default)]
pub struct RtpPcmauPay;
#[glib::object_subclass]
impl ObjectSubclass for RtpPcmauPay {
const ABSTRACT: bool = true;
const NAME: &'static str = "GstRtpPcmauPay2";
type Type = super::RtpPcmauPay;
type ParentType = crate::baseaudiopay::RtpBaseAudioPay2;
}
impl ObjectImpl for RtpPcmauPay {}
impl GstObjectImpl for RtpPcmauPay {}
impl ElementImpl for RtpPcmauPay {}
impl crate::basepay::RtpBasePay2Impl for RtpPcmauPay {
fn set_sink_caps(&self, caps: &gst::Caps) -> bool {
let s = caps.structure(0).unwrap();
let rate = u32::try_from(s.get::<i32>("rate").unwrap()).unwrap();
let src_caps = gst::Caps::builder("application/x-rtp")
.field("media", "audio")
.field(
"encoding-name",
if self.obj().type_() == super::RtpPcmaPay::static_type() {
"PCMA"
} else {
"PCMU"
},
)
.field("clock-rate", rate as i32)
.build();
self.obj().set_src_caps(&src_caps);
self.obj().set_bpf(1);
true
}
#[allow(clippy::single_match)]
fn sink_query(&self, query: &mut gst::QueryRef) -> bool {
match query.view_mut() {
gst::QueryViewMut::Caps(query) => {
let mut caps = self.obj().sink_pad().pad_template_caps();
// If the payload type is 0 or 8 then only 8000Hz are supported.
if [0, 8].contains(&self.obj().property::<u32>("pt")) {
let caps = caps.make_mut();
caps.set("rate", 8000);
}
if let Some(filter) = query.filter() {
caps = filter.intersect_with_mode(&caps, gst::CapsIntersectMode::First);
}
query.set_result(&caps);
return true;
}
_ => (),
}
self.parent_sink_query(query)
}
}
impl crate::baseaudiopay::RtpBaseAudioPay2Impl for RtpPcmauPay {}
/**
* SECTION:element-rtppcmapay2
* @see_also: rtppcmadepay2, rtppcmupay2, rtppcmudepay2, alawenc, alawdec
*
* Payloads A-law encoded audio into RTP packets as per [RFC 3551][rfc-3551].
*
* [rfc-3551]: https://www.rfc-editor.org/rfc/rfc3551.html#section-4.5.10
*
* ## Example pipeline
*
* |[
* gst-launch-1.0 audiotestsrc wave=ticks ! audio/x-raw,rate=8000,channels=1 ! alawenc ! rtppcmapay2 ! udpsink host=127.0.0.1 port=5004
* ]| This will generate an A-law audio test signal and payload it as RTP and send it out
* as UDP to localhost port 5004.
*
* Since: plugins-rs-0.13.0
*/
#[derive(Default)]
pub struct RtpPcmaPay;
#[glib::object_subclass]
impl ObjectSubclass for RtpPcmaPay {
const NAME: &'static str = "GstRtpPcmaPay2";
type Type = super::RtpPcmaPay;
type ParentType = super::RtpPcmauPay;
}
impl ObjectImpl for RtpPcmaPay {
fn constructed(&self) {
self.parent_constructed();
// Default to payload type 8
self.obj().set_property("pt", 8u32);
}
}
impl GstObjectImpl for RtpPcmaPay {}
impl ElementImpl for RtpPcmaPay {
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
gst::subclass::ElementMetadata::new(
"RTP PCMA Payloader",
"Codec/Payloader/Network/RTP",
"Payload A-law Audio into RTP packets (RFC 3551)",
"Sebastian Dröge <sebastian@centricular.com>",
)
});
Some(&*ELEMENT_METADATA)
}
fn pad_templates() -> &'static [gst::PadTemplate] {
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
let sink_pad_template = gst::PadTemplate::new(
"sink",
gst::PadDirection::Sink,
gst::PadPresence::Always,
&gst::Caps::builder("audio/x-alaw")
.field("channels", 1i32)
.field("rate", gst::IntRange::new(1i32, i32::MAX))
.build(),
)
.unwrap();
let src_pad_template = gst::PadTemplate::new(
"src",
gst::PadDirection::Src,
gst::PadPresence::Always,
&gst::Caps::builder_full()
.structure(
gst::Structure::builder("application/x-rtp")
.field("media", "audio")
.field("payload", 8i32)
.field("clock-rate", 8000i32)
.build(),
)
.structure(
gst::Structure::builder("application/x-rtp")
.field("media", "audio")
.field("payload", gst::IntRange::new(96i32, 127i32))
.field("encoding-name", "PCMA")
.field("clock-rate", gst::IntRange::new(1, i32::MAX))
.build(),
)
.build(),
)
.unwrap();
vec![src_pad_template, sink_pad_template]
});
PAD_TEMPLATES.as_ref()
}
}
impl crate::basepay::RtpBasePay2Impl for RtpPcmaPay {}
impl crate::baseaudiopay::RtpBaseAudioPay2Impl for RtpPcmaPay {}
impl super::RtpPcmauPayImpl for RtpPcmaPay {}
/**
* SECTION:element-rtppcmupay2
* @see_also: rtppcmudepay2, rtppcmapay2, rtppcmadepay2, mulawenc, mulawdec
*
* Payloads µ-law encoded audio into RTP packets as per [RFC 3551][rfc-3551].
*
* [rfc-3551]: https://www.rfc-editor.org/rfc/rfc3551.html#section-4.5.10
*
* ## Example pipeline
*
* |[
* gst-launch-1.0 audiotestsrc wave=ticks ! audio/x-raw,rate=8000,channels=1 ! mulawenc ! rtppcmupay2 ! udpsink host=127.0.0.1 port=5004
* ]| This will generate a µ-law audio test signal and payload it as RTP and send it out
* as UDP to localhost port 5004.
*
* Since: plugins-rs-0.13.0
*/
#[derive(Default)]
pub struct RtpPcmuPay;
#[glib::object_subclass]
impl ObjectSubclass for RtpPcmuPay {
const NAME: &'static str = "GstRtpPcmuPay2";
type Type = super::RtpPcmuPay;
type ParentType = super::RtpPcmauPay;
}
impl ObjectImpl for RtpPcmuPay {
fn constructed(&self) {
self.parent_constructed();
// Default to payload type 0
self.obj().set_property("pt", 0u32);
}
}
impl GstObjectImpl for RtpPcmuPay {}
impl ElementImpl for RtpPcmuPay {
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
gst::subclass::ElementMetadata::new(
"RTP PCMU Payloader",
"Codec/Payloader/Network/RTP",
"Payload µ-law Audio into RTP packets (RFC 3551)",
"Sebastian Dröge <sebastian@centricular.com>",
)
});
Some(&*ELEMENT_METADATA)
}
fn pad_templates() -> &'static [gst::PadTemplate] {
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
let sink_pad_template = gst::PadTemplate::new(
"sink",
gst::PadDirection::Sink,
gst::PadPresence::Always,
&gst::Caps::builder("audio/x-mulaw")
.field("channels", 1i32)
.field("rate", gst::IntRange::new(1i32, i32::MAX))
.build(),
)
.unwrap();
let src_pad_template = gst::PadTemplate::new(
"src",
gst::PadDirection::Src,
gst::PadPresence::Always,
&gst::Caps::builder_full()
.structure(
gst::Structure::builder("application/x-rtp")
.field("media", "audio")
.field("payload", 0i32)
.field("clock-rate", 8000i32)
.build(),
)
.structure(
gst::Structure::builder("application/x-rtp")
.field("media", "audio")
.field("payload", gst::IntRange::new(96i32, 127i32))
.field("encoding-name", "PCMU")
.field("clock-rate", gst::IntRange::new(1, i32::MAX))
.build(),
)
.build(),
)
.unwrap();
vec![src_pad_template, sink_pad_template]
});
PAD_TEMPLATES.as_ref()
}
}
impl crate::basepay::RtpBasePay2Impl for RtpPcmuPay {}
impl crate::baseaudiopay::RtpBaseAudioPay2Impl for RtpPcmuPay {}
impl super::RtpPcmauPayImpl for RtpPcmuPay {}

View file

@ -0,0 +1,59 @@
//
// Copyright (C) 2023 Sebastian Dröge <sebastian@centricular.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0
use gst::{glib, prelude::*, subclass::prelude::*};
pub mod imp;
glib::wrapper! {
pub struct RtpPcmauPay(ObjectSubclass<imp::RtpPcmauPay>)
@extends crate::baseaudiopay::RtpBaseAudioPay2, crate::basepay::RtpBasePay2, gst::Element, gst::Object;
}
pub trait RtpPcmauPayImpl: crate::baseaudiopay::RtpBaseAudioPay2Impl {}
unsafe impl<T: RtpPcmauPayImpl> IsSubclassable<T> for RtpPcmauPay {
fn class_init(class: &mut glib::Class<Self>) {
Self::parent_class_init::<T>(class);
}
}
glib::wrapper! {
pub struct RtpPcmaPay(ObjectSubclass<imp::RtpPcmaPay>)
@extends RtpPcmauPay, crate::baseaudiopay::RtpBaseAudioPay2, crate::basepay::RtpBasePay2, gst::Element, gst::Object;
}
glib::wrapper! {
pub struct RtpPcmuPay(ObjectSubclass<imp::RtpPcmuPay>)
@extends RtpPcmauPay, crate::baseaudiopay::RtpBaseAudioPay2, crate::basepay::RtpBasePay2, gst::Element, gst::Object;
}
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
#[cfg(feature = "doc")]
{
use gst::prelude::*;
// Make internal base class available in docs
crate::pcmau::pay::RtpPcmauPay::static_type()
.mark_as_plugin_api(gst::PluginAPIFlags::empty());
}
gst::Element::register(
Some(plugin),
"rtppcmapay2",
gst::Rank::MARGINAL,
RtpPcmaPay::static_type(),
)?;
gst::Element::register(
Some(plugin),
"rtppcmupay2",
gst::Rank::MARGINAL,
RtpPcmuPay::static_type(),
)
}

239
net/rtp/src/pcmau/tests.rs Normal file
View file

@ -0,0 +1,239 @@
//
// Copyright (C) 2023 Sebastian Dröge <sebastian@centricular.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0
use crate::tests::{run_test_pipeline, ExpectedBuffer, ExpectedPacket, Source};
use std::cmp;
fn init() {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| {
gst::init().unwrap();
crate::plugin_register_static().expect("rtppcmau test");
});
}
#[test]
fn test_pcma() {
init();
let src = "audiotestsrc num-buffers=100 samplesperbuffer=400 ! audio/x-raw,rate=8000,channels=1 ! alawenc";
let pay = "rtppcmapay2";
let depay = "rtppcmadepay2";
let mut expected_pay = Vec::with_capacity(100);
for i in 0..100 {
expected_pay.push(vec![ExpectedPacket::builder()
.pts(gst::ClockTime::from_mseconds(i * 50))
.flags(if i == 0 {
gst::BufferFlags::DISCONT | gst::BufferFlags::MARKER
} else {
gst::BufferFlags::empty()
})
.pt(8)
.rtp_time(((i * 400) & 0xffff_ffff) as u32)
.marker_bit(i == 0)
.build()]);
}
let mut expected_depay = Vec::with_capacity(100);
for i in 0..100 {
expected_depay.push(vec![ExpectedBuffer::builder()
.pts(gst::ClockTime::from_mseconds(i * 50))
.size(400)
.flags(if i == 0 {
gst::BufferFlags::DISCONT | gst::BufferFlags::RESYNC
} else {
gst::BufferFlags::empty()
})
.build()]);
}
run_test_pipeline(Source::Bin(src), pay, depay, expected_pay, expected_depay);
}
#[test]
fn test_pcma_splitting() {
init();
let src = "audiotestsrc num-buffers=100 samplesperbuffer=480 ! audio/x-raw,rate=8000,channels=1 ! alawenc";
let pay = "rtppcmapay2 min-ptime=25000000 max-ptime=50000000";
let depay = "rtppcmadepay2";
// Every input buffer is 480 samples, every packet can contain between 200 and 400 samples
// so a bit of splitting is necessary and sometimes the remaining queued data ends up filling
// one additional packet
let mut expected_pay = Vec::with_capacity(134);
let mut queued = 0;
let mut pos = 0;
for i in 0..134 {
if i < 100 {
queued += 480;
}
while (i < 100 && queued >= 200) || (i == 100 && queued > 0) {
let size = cmp::min(queued, 400);
queued -= size;
expected_pay.push(vec![ExpectedPacket::builder()
.pts(gst::ClockTime::from_mseconds(pos / 8))
.size(size as usize + 12)
.flags(if pos == 0 {
gst::BufferFlags::DISCONT | gst::BufferFlags::MARKER
} else {
gst::BufferFlags::empty()
})
.pt(8)
.rtp_time((pos & 0xffff_ffff) as u32)
.marker_bit(pos == 0)
.build()]);
pos += size;
}
}
let mut expected_depay = Vec::with_capacity(134);
for packets in &expected_pay {
for packet in packets {
expected_depay.push(vec![ExpectedBuffer::builder()
.pts(packet.pts)
.maybe_size(packet.size.map(|size| size - 12))
.flags(if packet.pts.is_zero() {
gst::BufferFlags::DISCONT | gst::BufferFlags::RESYNC
} else {
gst::BufferFlags::empty()
})
.build()]);
}
}
run_test_pipeline(Source::Bin(src), pay, depay, expected_pay, expected_depay);
}
#[test]
fn test_pcma_discont() {
init();
let caps = gst::Caps::builder("audio/x-alaw")
.field("channels", 1)
.field("rate", 8000i32)
.build();
let mut buffers = Vec::with_capacity(10);
let mut pos = 0;
// First 5 buffers are normal, then a 10s jump
for _ in 0..10 {
let mut buffer = gst::Buffer::with_size(400).unwrap();
{
let buffer = buffer.get_mut().unwrap();
buffer.set_pts(gst::ClockTime::from_mseconds(pos / 8));
}
buffers.push(buffer);
pos += 400;
if pos == 2000 {
pos += 80000;
}
}
let pay = "rtppcmapay2 discont-wait=25000000";
let depay = "rtppcmadepay2";
let mut expected_pay = Vec::with_capacity(10);
let mut pos = 0;
for _ in 0..10 {
expected_pay.push(vec![ExpectedPacket::builder()
.pts(gst::ClockTime::from_mseconds(pos / 8))
.size(412)
.flags(if pos == 0 {
gst::BufferFlags::DISCONT | gst::BufferFlags::MARKER
} else if pos == 82000 {
gst::BufferFlags::MARKER
} else {
gst::BufferFlags::empty()
})
.pt(8)
.rtp_time((pos & 0xffff_ffff) as u32)
.marker_bit(pos == 0 || pos == 82000)
.build()]);
pos += 400;
if pos == 2000 {
pos += 80000;
}
}
let mut expected_depay = Vec::with_capacity(10);
for packets in &expected_pay {
for packet in packets {
expected_depay.push(vec![ExpectedBuffer::builder()
.pts(packet.pts)
.maybe_size(packet.size.map(|size| size - 12))
.flags(if packet.pts.is_zero() {
gst::BufferFlags::DISCONT | gst::BufferFlags::RESYNC
} else if packet.flags.contains(gst::BufferFlags::MARKER) {
gst::BufferFlags::RESYNC
} else {
gst::BufferFlags::empty()
})
.build()]);
}
}
run_test_pipeline(
Source::Buffers(caps, buffers),
pay,
depay,
expected_pay,
expected_depay,
);
}
#[test]
fn test_pcmu() {
init();
let src = "audiotestsrc num-buffers=100 samplesperbuffer=400 ! audio/x-raw,rate=8000,channels=1 ! mulawenc";
let pay = "rtppcmupay2";
let depay = "rtppcmudepay2";
let mut expected_pay = Vec::with_capacity(100);
for i in 0..100 {
expected_pay.push(vec![ExpectedPacket::builder()
.pts(gst::ClockTime::from_mseconds(i * 50))
.size(412)
.flags(if i == 0 {
gst::BufferFlags::DISCONT | gst::BufferFlags::MARKER
} else {
gst::BufferFlags::empty()
})
.pt(0)
.rtp_time(((i * 400) & 0xffff_ffff) as u32)
.marker_bit(i == 0)
.build()]);
}
let mut expected_depay = Vec::with_capacity(100);
for i in 0..100 {
expected_depay.push(vec![ExpectedBuffer::builder()
.pts(gst::ClockTime::from_mseconds(i * 50))
.size(400)
.flags(if i == 0 {
gst::BufferFlags::DISCONT | gst::BufferFlags::RESYNC
} else {
gst::BufferFlags::empty()
})
.build()]);
}
run_test_pipeline(Source::Bin(src), pay, depay, expected_pay, expected_depay);
}

515
net/rtp/src/tests.rs Normal file
View file

@ -0,0 +1,515 @@
//
// Copyright (C) 2023 Sebastian Dröge <sebastian@centricular.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0
use std::{
mem,
sync::{Arc, Mutex},
};
use gst::prelude::*;
/// Expected packet produced by the payloader
pub struct ExpectedPacket {
/// All packets are expected to have a known and fixed PTS.
pub pts: gst::ClockTime,
/// If not set the size will not be checked.
pub size: Option<usize>,
pub flags: gst::BufferFlags,
pub pt: u8,
pub rtp_time: u32,
pub marker: bool,
}
impl ExpectedPacket {
/// Creates a builder for an `ExpectedPacket`.
///
/// Assigns the following packet default values:
///
/// * pts: gst::ClockTime::ZERO
/// * size: None => not checked
/// * flags: gst::BufferFlags::empty()
/// * pt: 96
/// * rtp_time: 0
/// * marker: true
pub fn builder() -> ExpectedPacketBuilder {
ExpectedPacketBuilder(ExpectedPacket {
pts: gst::ClockTime::ZERO,
size: None,
flags: gst::BufferFlags::empty(),
pt: 96,
rtp_time: 0,
marker: true,
})
}
}
pub struct ExpectedPacketBuilder(ExpectedPacket);
impl ExpectedPacketBuilder {
pub fn pts(mut self, pts: gst::ClockTime) -> Self {
self.0.pts = pts;
self
}
pub fn size(mut self, size: usize) -> Self {
self.0.size = Some(size);
self
}
pub fn flags(mut self, flags: gst::BufferFlags) -> Self {
self.0.flags = flags;
self
}
pub fn pt(mut self, pt: u8) -> Self {
self.0.pt = pt;
self
}
pub fn rtp_time(mut self, rtp_time: u32) -> Self {
self.0.rtp_time = rtp_time;
self
}
pub fn marker_bit(mut self, marker: bool) -> Self {
self.0.marker = marker;
self
}
pub fn build(self) -> ExpectedPacket {
self.0
}
}
/// Expected buffer produced by the depayloader
#[derive(Debug)]
pub struct ExpectedBuffer {
/// If not set then it is asserted that the depayloaded buffer also has no PTS.
pub pts: Option<gst::ClockTime>,
/// If not set then it is asserted that the depayloaded buffer also has no DTS.
pub dts: Option<gst::ClockTime>,
/// If not set the size will not be checked.
pub size: Option<usize>,
pub flags: gst::BufferFlags,
}
impl ExpectedBuffer {
/// Creates a builder for an `ExpectedBuffer`.
///
/// Assigns the following buffer default values:
///
/// * pts: None
/// * dts: None
/// * size: None => not checked
/// * flags: gst::BufferFlags::empty()
pub fn builder() -> ExpectedBufferBuilder {
ExpectedBufferBuilder(ExpectedBuffer {
pts: None,
dts: None,
size: None,
flags: gst::BufferFlags::empty(),
})
}
}
pub struct ExpectedBufferBuilder(ExpectedBuffer);
#[allow(dead_code)]
impl ExpectedBufferBuilder {
pub fn pts(mut self, pts: gst::ClockTime) -> Self {
self.0.pts = Some(pts);
self
}
pub fn maybe_pts(mut self, pts: Option<gst::ClockTime>) -> Self {
self.0.pts = pts;
self
}
pub fn dts(mut self, dts: gst::ClockTime) -> Self {
self.0.dts = Some(dts);
self
}
pub fn size(mut self, size: usize) -> Self {
self.0.size = Some(size);
self
}
pub fn maybe_size(mut self, size: Option<usize>) -> Self {
self.0.size = size;
self
}
pub fn flags(mut self, flags: gst::BufferFlags) -> Self {
self.0.flags = flags;
self
}
pub fn build(self) -> ExpectedBuffer {
self.0
}
}
/// Source of the test
pub enum Source<'a> {
#[allow(dead_code)]
Buffers(gst::Caps, Vec<gst::Buffer>),
Bin(&'a str),
}
/// Pipeline wrapper to automatically set state to `Null` on drop
///
/// Useful to not get critical warnings on panics from unwinding.
struct Pipeline(gst::Pipeline);
impl Drop for Pipeline {
fn drop(&mut self) {
let _ = self.0.set_state(gst::State::Null);
}
}
impl std::ops::Deref for Pipeline {
type Target = gst::Pipeline;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub fn run_test_pipeline(
src: Source,
pay: &str,
depay: &str,
expected_pay: Vec<Vec<ExpectedPacket>>,
expected_depay: Vec<Vec<ExpectedBuffer>>,
) {
let pipeline = Pipeline(gst::Pipeline::new());
// Return if the pipelines can't be built: this likely means that encoders are missing
let src = match src {
Source::Bin(src) => {
let Ok(src) = gst::parse::bin_from_description_with_name(src, true, "rtptestsrc")
else {
return;
};
src.upcast::<gst::Element>()
}
Source::Buffers(caps, buffers) => {
let mut buffers = buffers.into_iter();
let appsrc = gst_app::AppSrc::builder()
.format(gst::Format::Time)
.caps(&caps)
.callbacks(
gst_app::AppSrcCallbacks::builder()
.need_data(move |appsrc, _offset| {
let Some(buffer) = buffers.next() else {
let _ = appsrc.end_of_stream();
return;
};
// appsrc already handles the error for us
let _ = appsrc.push_buffer(buffer);
})
.build(),
)
.build();
appsrc.upcast::<gst::Element>()
}
};
let Ok(pay) = gst::parse::bin_from_description_with_name(pay, true, "rtptestpay") else {
return;
};
let pay = pay.upcast::<gst::Element>();
// Collect samples from after the payloader
let pay_samples = Arc::new(Mutex::new(Vec::new()));
pay.static_pad("src")
.unwrap()
.add_probe(
gst::PadProbeType::BUFFER | gst::PadProbeType::BUFFER_LIST,
{
let pay_samples = pay_samples.clone();
move |pad, info| {
let segment_event = pad.sticky_event::<gst::event::Segment>(0).unwrap();
let segment = segment_event.segment().clone();
let caps = pad.current_caps().unwrap();
let mut sample_builder = gst::Sample::builder().segment(&segment).caps(&caps);
if let Some(buffer) = info.buffer() {
sample_builder = sample_builder.buffer(buffer);
} else if let Some(list) = info.buffer_list() {
sample_builder = sample_builder.buffer_list(list);
} else {
unreachable!();
}
pay_samples.lock().unwrap().push(sample_builder.build());
gst::PadProbeReturn::Ok
}
},
)
.unwrap();
let Ok(depay) = gst::parse::bin_from_description_with_name(depay, true, "rtptestdepay") else {
return;
};
let depay = depay.upcast::<gst::Element>();
let depay_samples = Arc::new(Mutex::new(Vec::new()));
let appsink = gst_app::AppSink::builder()
.sync(false)
.buffer_list(true)
.callbacks(
gst_app::AppSinkCallbacks::builder()
.new_sample({
let depay_samples = depay_samples.clone();
move |appsink| {
let Ok(sample) = appsink.pull_sample() else {
return Err(gst::FlowError::Flushing);
};
depay_samples.lock().unwrap().push(sample);
Ok(gst::FlowSuccess::Ok)
}
})
.build(),
)
.build();
pipeline
.add_many([&src, &pay, &depay, appsink.as_ref()])
.unwrap();
gst::Element::link_many([&src, &pay, &depay, appsink.as_ref()]).unwrap();
pipeline
.set_state(gst::State::Playing)
.expect("Failed to set pipeline to Playing");
let msg = pipeline
.bus()
.unwrap()
.timed_pop_filtered(
gst::ClockTime::NONE,
&[gst::MessageType::Error, gst::MessageType::Eos],
)
.expect("Didn't receive ERROR or EOS message");
assert_ne!(
msg.type_(),
gst::MessageType::Error,
"Received error message {msg:?}"
);
pipeline
.set_state(gst::State::Null)
.expect("Failed to set pipeline to Null");
drop(msg);
drop(src);
drop(pay);
drop(depay);
drop(appsink);
drop(pipeline);
let pay_samples = mem::take(&mut *pay_samples.lock().unwrap());
let depay_samples = mem::take(&mut *depay_samples.lock().unwrap());
// Now check against the expected values
assert_eq!(
pay_samples.len(),
expected_pay.len(),
"Expected {} payload packets but got {}",
expected_pay.len(),
pay_samples.len()
);
let mut initial_timestamp = None;
for (i, (expected_list, sample)) in
Iterator::zip(expected_pay.into_iter(), pay_samples.into_iter()).enumerate()
{
let mut iter_a;
let mut iter_b;
let buffer_iter: &mut dyn Iterator<Item = &gst::BufferRef> =
if let Some(list) = sample.buffer_list() {
assert_eq!(
list.len(),
expected_list.len(),
"Expected {} buffers in {}-th payload list but got {}",
expected_list.len(),
i,
list.len()
);
iter_a = list.iter();
&mut iter_a
} else {
let buffer = sample.buffer().unwrap();
assert_eq!(
expected_list.len(),
1,
"Expected {} buffers in {}-th payload list but got 1",
expected_list.len(),
i,
);
iter_b = Some(buffer).into_iter();
&mut iter_b
};
for (j, (expected_buffer, buffer)) in
Iterator::zip(expected_list.into_iter(), buffer_iter).enumerate()
{
let buffer_pts = buffer.pts().expect("Buffer without PTS");
assert_eq!(
buffer_pts, expected_buffer.pts,
"Buffer {} of payload buffer list {} has unexpected PTS {} instead of {}",
j, i, buffer_pts, expected_buffer.pts,
);
if let Some(expected_size) = expected_buffer.size {
assert_eq!(
buffer.size(),
expected_size,
"Buffer {} of payload buffer list {} has unexpected size {} instead of {}",
j,
i,
buffer.size(),
expected_size,
);
}
let buffer_flags = buffer.flags() - gst::BufferFlags::TAG_MEMORY;
assert_eq!(
buffer_flags, expected_buffer.flags,
"Buffer {} of payload buffer list {} has unexpected flags {:?} instead of {:?}",
j, i, buffer_flags, expected_buffer.flags,
);
let map = buffer.map_readable().unwrap();
let rtp_packet = rtp_types::RtpPacket::parse(&map).expect("Invalid RTP packet");
assert_eq!(
rtp_packet.payload_type(),
expected_buffer.pt,
"Buffer {} of payload buffer list {} has unexpected payload type {:?} instead of {:?}",
j,
i,
rtp_packet.payload_type(),
expected_buffer.pt,
);
assert_eq!(
rtp_packet.marker_bit(),
expected_buffer.marker,
"Buffer {} of payload buffer list {} has unexpected marker {:?} instead of {:?}",
j,
i,
rtp_packet.marker_bit(),
expected_buffer.marker,
);
if initial_timestamp.is_none() {
initial_timestamp = Some(rtp_packet.timestamp());
}
let initial_timestamp = initial_timestamp.unwrap();
let expected_timestamp = expected_buffer.rtp_time.wrapping_add(initial_timestamp);
assert_eq!(
rtp_packet.timestamp(),
expected_timestamp,
"Buffer {} of payload buffer list {} has unexpected RTP timestamp {:?} instead of {:?}",
j,
i,
rtp_packet.timestamp(),
expected_timestamp,
);
}
}
assert_eq!(
depay_samples.len(),
expected_depay.len(),
"Expected {} depayload samples but got {}",
expected_depay.len(),
depay_samples.len()
);
for (i, (expected_list, sample)) in
Iterator::zip(expected_depay.into_iter(), depay_samples.into_iter()).enumerate()
{
let mut iter_a;
let mut iter_b;
let buffer_iter: &mut dyn Iterator<Item = &gst::BufferRef> =
if let Some(list) = sample.buffer_list() {
assert_eq!(
list.len(),
expected_list.len(),
"Expected {} depayload buffers in {}-th list but got {}",
expected_list.len(),
i,
list.len()
);
iter_a = list.iter();
&mut iter_a
} else {
let buffer = sample.buffer().unwrap();
assert_eq!(
expected_list.len(),
1,
"Expected {} depayload buffers in {}-th list but got 1",
expected_list.len(),
i,
);
iter_b = Some(buffer).into_iter();
&mut iter_b
};
for (j, (expected_buffer, buffer)) in
Iterator::zip(expected_list.into_iter(), buffer_iter).enumerate()
{
let buffer_pts = buffer.pts();
assert_eq!(
buffer_pts,
expected_buffer.pts,
"Buffer {} of depayload buffer list {} has unexpected PTS {} instead of {}",
j,
i,
buffer_pts.display(),
expected_buffer.pts.display(),
);
if let Some(expected_size) = expected_buffer.size {
assert_eq!(
buffer.size(),
expected_size,
"Buffer {} of depayload buffer list {} has unexpected size {} instead of {}",
j,
i,
buffer.size(),
expected_size,
);
}
let buffer_flags = buffer.flags() - gst::BufferFlags::TAG_MEMORY;
assert_eq!(
buffer_flags, expected_buffer.flags,
"Buffer {} of depayload buffer list {} has unexpected flags {:?} instead of {:?}",
j, i, buffer_flags, expected_buffer.flags,
);
}
}
}