├── LICENSE.md ├── README.md ├── capture ├── README.md ├── capture.sh ├── dpdk │ ├── README.md │ ├── bpf.c │ ├── bpf_no_ptp.c │ ├── dpdk-capture-diagram.jpg │ ├── dpdk-capture.sh │ ├── install.sh │ └── libpcap-HW-TS-support-for-dpdk.patch ├── get_sdp.py ├── install.sh ├── network_setup.sh ├── nic_setup.sh ├── rtcpdump.sh └── socket_reader.c ├── config ├── bashrc ├── nginx.conf ├── st2110.bashrc ├── st2110.conf └── st2110.init ├── doc ├── SW_source.md ├── closed_captions.md ├── embrionix.md ├── hw_encoding.md ├── nmos.md ├── scte_104_to_35.md ├── sdp.sample ├── transcoder_perf.md └── troubleshoot.md ├── ebu-list ├── README.md ├── capture_probe │ ├── capture.js │ └── tcpdump.js ├── ebu_list_ctl ├── ebulist-probe.init ├── ebulist-probe.service ├── ebulist.service ├── freerun.sh └── install.sh ├── install.sh ├── nmos ├── Dockerfile ├── README.md ├── api_patch.py ├── connection_loop.py ├── curl-format.txt ├── get_sdp.sh ├── install.sh ├── multi_nmos_node_conf_gen.sh ├── nmos.init ├── nmos.json ├── nmos_node.py ├── nmos_st2110_monitor_ctl ├── node_connection.py ├── node_controller.py ├── node_poller.py └── patch_sdp.sh ├── pcap ├── ancillary_editor.py ├── audio_raw_extractor.py ├── pkt_drop_detector.py └── video_yuv_extractor.py ├── ptp ├── 0001-port-do-not-answer-in-case-of-unknown-mgt-message-co.patch ├── README.md ├── install.sh ├── linuxptp_sync_graph.py ├── ptp.init ├── ptp4l.conf ├── ptp4l_lab.conf ├── ptp4l_orig.conf ├── ptp4l_st2110.conf └── undesired_mgt_msg.pcapng └── transcoder ├── Dockerfile ├── README.md ├── ffmpeg-avformat-rtp-compute-smpte2110-timestamps.patch ├── ffmpeg-avutil-smpte2110-add-helpers-to-compute-PTS.patch ├── ffmpeg-force-input-threading.patch ├── install.sh ├── transcode.sh └── transcoder_stats.sh /README.md: -------------------------------------------------------------------------------- 1 | # ST 2110 software toolkit 2 | 3 | This toolkit aims at capturing, analysing and transcoding SMPTE ST 2110 streams. 4 | 5 | Features: 6 | 7 | * capture RTP packets with high precision timestamps 8 | * transcode ST 2110 essences to h264 9 | * provide various pcap tools 10 | * provide a recipe to create a live version of [EBU-LIST](https://tech.ebu.ch/list) 11 | * troubleshoot the network by capturing traffic on remote hosts 12 | 13 | Sponsored by: 14 | 15 | ![logo](https://site-cbc.radio-canada.ca/site/annual-reports/2014-2015/_images/about/services/cbc-radio-canada.png) 16 | 17 | Tested distros: 18 | 19 | * Centos 7 20 | * Dockerized Centos 7 21 | * Ubuntu > 20.04 22 | 23 | ## Install 24 | 25 | The repo contains multiple install sub-scripts, use the one in TOP 26 | DIRECTORY ONLY. 27 | 28 | ```sh 29 | $ ./install.sh 30 | Usage: ./install.sh
31 | sections are: 32 | * common: compile tools, network utilities, config 33 | * ptp: linuxptp 34 | * transcoder: ffmpeg, x264, mp3 and other codecs 35 | * capture: dpdk-based capture engine 36 | * ebulist: EBU-LIST pcap analyzer, NOT tested for a while 37 | * nmos: Sony nmos-cpp node and scripts for SDP patching 38 | 39 | Regardless of your setup, please install 'common' section first. 40 | ``` 41 | 42 | ## Configuration 43 | 44 | Both capture and transcoder scripts have default parameters but they can 45 | be overriden by a config file to be installed as `/etc/st2110.conf`. 46 | See the [sample](./config/st2110.conf). This config also provisions an 47 | EBU-LIST server in live mode, i.e. connected to a ST 2110 network. 48 | 49 | ## Capture 50 | 51 | These [instructions](./capture/README.md) 52 | show how to setup a performant stream capture engine based on Nvidia/Mellanox NIC + DPDK. 53 | 54 | [rtcdump](./capture/rtcpdump.sh) is standalone remote capture tool for 55 | generic network issue. 56 | 57 | ## Transcode 58 | 59 | It is required to go through the capture process before in order to 60 | validate all the underlying layers forwards a stream to an application. 61 | Then one can use our FFmpeg-based transcoder following these 62 | [instructions.](./transcoder/README.md) 63 | 64 | ## EBU-LIST 65 | 66 | Follow the [integration guide](./ebu-list/README.md) for a complete capture and analysis solution. 67 | 68 | ## NMOS 69 | 70 | [README](./nmos/README.md) shows a POC for a NMOSisfied transcoder. And 71 | various scripts are propose to get SDP file from source and patch them 72 | to destination. 73 | 74 | ## Pcap tools 75 | 76 | [Pcap folder](./pcap) contains helper scripts which operate on PCAP files: 77 | 78 | * ancillary editor: insert different types of failure in SMPTE ST 291-1 payload 79 | * RTP pkt drop detector: count packets and drops for every (src/dst) IP pair found in a given pcap file 80 | * video yuv extractor: convert RFC4175 payload into raw YUV file 81 | * audio extractor: convert AES 67 payload into raw file 82 | 83 | Dependencies: 84 | 85 | * python3 and pip 86 | * [scapy](https://scapy.net/) 87 | * [bitstruct](https://pypi.org/project/bitstruct/) 88 | 89 | ## TODO 90 | 91 | * test the RFC4175 encoder in `ffmpeg` 92 | * build a docker image for transcoder 93 | * test recent version of `linux-ptp` to validate that `pmc` no longer needs root permission 94 | * rework`./capture/nic_setup.sh` 95 | * nmos-poller: display ffmpeg status 96 | 97 | ## [Troubleshoot](./doc/troubleshoot.md) 98 | 99 | ## Additional resources 100 | 101 | * [video](https://github.com/FOXNEOAdvancedTechnology/smpte2110-20-dissector) 102 | * [ancillary](https://github.com/FOXNEOAdvancedTechnology/smpte2110-40-dissector) 103 | * [EBU tools](https://github.com/ebu/smpte2110-analyzer) 104 | -------------------------------------------------------------------------------- /capture/README.md: -------------------------------------------------------------------------------- 1 | # Capture 2 | 3 | 4 | ## Nvidia/Mellanox software 5 | 6 | [OpenFabric OFED driver](https://docs.nvidia.com/networking/display/MLNXOFEDv461000/Release+Notes) 7 | is no more supported but everything needed for a good RDMA-accelerated packet capture is now 8 | included in Ubuntu packages: `rdma-core`, `libibverbs`, while 9 | [mft](https://network.nvidia.com/products/adapter-software/firmware-tools/) 10 | is a hardware-utility toolbox supported by Nvidia. 11 | 12 | ## DPDK-based capture engine 13 | 14 | [DPDK page](https://github.com/pkeroulas/st2110-toolkit/blob/master/capture/dpdk/README.md). 15 | 16 | 17 | ## Various tools 18 | 19 | The following scripts were just helpers when epxerimenting with SDP, NIC setup, multicast joining and basic capture. 20 | 21 | * `nic_setup.sh` 22 | * `./get_sdp.py` 23 | * `./network_setup.sh` 24 | * `./capture.sh` 25 | 26 | ## [Troubleshoot](../doc/troubleshoot.md) 27 | -------------------------------------------------------------------------------- /capture/capture.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # DEPRECATED 4 | # 5 | # This script wraps around tcpdump with multicast group join/leave 6 | # Not suitable for ST 2110 streams. 7 | 8 | # default param 9 | CAPTURE_DURATION=10 # in sec 10 | ST2110_CONF_FILE=/etc/st2110.conf 11 | MEDIA_IFACE=eth0 12 | CAPTURE_TRUNCATE=no 13 | 14 | # const 15 | CAPTURE=tmp.pcap 16 | MAX_COUNT=100000 17 | SCRIPT=$(basename $0) 18 | DIR=$(dirname $0) 19 | 20 | help() { 21 | echo -e " 22 | $SCRIPT joins multicast groups and captures the incoming traffic in 23 | file: __.pcap. The user must have 24 | privileged rights. Tcpdump command uses 'adapter_unsynced' to let the 25 | NIC timestamp the arriving packet. 26 | 27 | Usage: 28 | \t$SCRIPT help 29 | \t$SCRIPT setup 30 | \t$SCRIPT sdp 31 | \t$SCRIPT manual " 32 | } 33 | 34 | if [ -z $1 ]; then 35 | help 36 | exit 1 37 | fi 38 | 39 | # override params with possibly existing conf file 40 | if [ -f $ST2110_CONF_FILE ]; then 41 | source $ST2110_CONF_FILE 42 | fi 43 | 44 | cmd=$1 45 | shift 46 | 47 | case $cmd in 48 | help) 49 | help 50 | exit 1 51 | ;; 52 | setup) 53 | if [ $# -lt 2 ]; then 54 | help 55 | exit 1 56 | fi 57 | MEDIA_IFACE=$1 58 | sdp=$2 59 | $DIR/network_setup.sh $MEDIA_IFACE $sdp 60 | exit $? 61 | ;; 62 | sdp) 63 | if [ $# -eq 0 ]; then 64 | help 65 | exit 1 66 | fi 67 | 68 | sdp=$1 69 | if [ ! -f $sdp ]; then 70 | echo "$sdp is not a file" 71 | exit 1 72 | fi 73 | 74 | source_ip=$(sed -n 's/^o=.*IN IP4 \(.*\)$/\1/p' $sdp | head -1) 75 | mcast_ips=$(sed -n 's/^a=.*IN IP4 \(.*\) .*$/\1/p' $sdp) 76 | ;; 77 | manual) 78 | if [ $# -lt 2 ]; then 79 | help 80 | exit 1 81 | fi 82 | 83 | mcast_ips=$1 84 | CAPTURE_DURATION=$2 85 | source_ip="unknown" 86 | ;; 87 | *) 88 | help 89 | exit 1 90 | ;; 91 | esac 92 | 93 | echo "------------------------------------------" 94 | 95 | if [ ! -d /sys/class/net/$MEDIA_IFACE ]; then 96 | echo "$MEDIA_IFACE doesn't exist, exit." 97 | exit 1 98 | fi 99 | 100 | if [ $(cat /sys/class/net/$MEDIA_IFACE/operstate) != "up" ]; then 101 | echo "$MEDIA_IFACE is not up, exit." 102 | exit 1 103 | fi 104 | 105 | echo "$MEDIA_IFACE: OK" 106 | 107 | echo "------------------------------------------ 108 | Mcast IPs:" 109 | 110 | if [ -z "$mcast_ips" ]; then 111 | echo "Missing multicast group, exit." 112 | exit 1 113 | fi 114 | 115 | echo "$mcast_ips" | tr ' ' '\n' 116 | 117 | echo "------------------------------------------ 118 | Joining" 119 | 120 | for m in $mcast_ips; do 121 | smcroutectl join $MEDIA_IFACE $m 122 | if netstat -ng | grep -q "$MEDIA_IFACE.*$m"; then 123 | echo "$m OK" 124 | else 125 | echo "Can't joint $m" 126 | fi 127 | done 128 | 129 | echo "------------------------------------------ 130 | Capturing" 131 | 132 | if [ "$CAPTURE_TRUNCATE" = "yes" ]; then 133 | TCPDUMP_OPTIONS="--snapshot-length=100" 134 | fi 135 | 136 | tcpdump -vvv \ 137 | $TCPDUMP_OPTIONS \ 138 | -j adapter_unsynced \ 139 | -i $MEDIA_IFACE \ 140 | -n "multicast" \ 141 | -c $MAX_COUNT \ 142 | -w $CAPTURE \ 143 | & 144 | tcpdump_pid=$! 145 | 146 | for i in $(seq $CAPTURE_DURATION); do 147 | echo "$i sec ..." 148 | sleep 1 149 | done 150 | 151 | kill -0 $tcpdump_pid 2>/dev/null && kill $tcpdump_pid 152 | 153 | echo "------------------------------------------ 154 | Leaving" 155 | 156 | for m in $mcast_ips; do 157 | smcroutectl leave $MEDIA_IFACE $m 158 | done 159 | 160 | if [ ! -f $CAPTURE ]; then 161 | echo "No capture file." 162 | exit 1 163 | fi 164 | 165 | FILENAME="$(date +%F_%T)_$(hostname)_$(echo $source_ip | tr . _ | sed 's/\r//').pcap" 166 | mv $CAPTURE $FILENAME 167 | 168 | echo "------------------------------------------ 169 | Output file: 170 | $(du -h $FILENAME)" 171 | -------------------------------------------------------------------------------- /capture/dpdk/bpf.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-3-Clause 2 | * Copyright(c) 2018 Intel Corporation 3 | */ 4 | 5 | /* 6 | * eBPF program sample. 7 | * Accepts pointer to first segment packet data as an input parameter. 8 | * analog of tcpdump -s 1 -d 'dst 1.2.3.4 && udp && dst port 5000' 9 | * (000) ldh [12] 10 | * (001) jeq #0x800 jt 2 jf 12 11 | * (002) ld [30] 12 | * (003) jeq #0x1020304 jt 4 jf 12 13 | * (004) ldb [23] 14 | * (005) jeq #0x11 jt 6 jf 12 15 | * (006) ldh [20] 16 | * (007) jset #0x1fff jt 12 jf 8 17 | * (008) ldxb 4*([14]&0xf) 18 | * (009) ldh [x + 16] 19 | * (010) jeq #0x1388 jt 11 jf 12 20 | * (011) ret #1 21 | * (012) ret #0 22 | * 23 | * To compile on x86: 24 | * clang -O2 -U __GNUC__ -target bpf -c t1.c 25 | * 26 | * To compile on ARM: 27 | * clang -O2 -I/usr/include/aarch64-linux-gnu/ -target bpf -c t1.c 28 | */ 29 | 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | 36 | uint64_t 37 | entry(void *pkt) 38 | { 39 | struct ether_header *ether_header = (void *)pkt; 40 | 41 | if (ether_header->ether_type != htons(0x0800)) 42 | return 0; 43 | 44 | struct iphdr *iphdr = (void *)(ether_header + 1); 45 | if (iphdr->protocol != 17 || (iphdr->frag_off & 0x1ffff) != 0 || 46 | iphdr->daddr != htonl(0x1020304)) 47 | return 0; 48 | 49 | int hlen = iphdr->ihl * 4; 50 | struct udphdr *udphdr = (void *)iphdr + hlen; 51 | 52 | if (udphdr->dest != htons(5000)) 53 | return 0; 54 | 55 | return 1; 56 | } 57 | -------------------------------------------------------------------------------- /capture/dpdk/bpf_no_ptp.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD-3-Clause 2 | * Copyright(c) 2018 Intel Corporation 3 | */ 4 | 5 | /* 6 | * eBPF program sample to filter ptp traffic (dst 224.0.1.129) 7 | * 8 | * To compile on x86: 9 | * install ibc6-dev-i386 10 | * clang -O2 -U __GNUC__ -I /usr/include/x86_64-linux-gnu/ -target bpf -c bpf_no_ptp.c 11 | * 12 | */ 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | uint64_t 21 | entry(void *pkt) 22 | { 23 | struct ether_header *ether_header = (void *)pkt; 24 | 25 | if (ether_header->ether_type != htons(0x0800)) 26 | return 0; 27 | 28 | struct iphdr *iphdr = (void *)(ether_header + 1); 29 | if (iphdr->protocol != 17 || (iphdr->frag_off & 0x1ffff) != 0 || iphdr->daddr == htonl(0xE0000181)) 30 | return 0; 31 | 32 | return 1; 33 | } 34 | -------------------------------------------------------------------------------- /capture/dpdk/dpdk-capture-diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkeroulas/st2110-toolkit/f5a18617f9d4ee8bfe8fd5bed6de765cb8d42465/capture/dpdk/dpdk-capture-diagram.jpg -------------------------------------------------------------------------------- /capture/dpdk/dpdk-capture.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | usage(){ 4 | echo "$0 interprets tcpdump-like parameters and passes them to 5 | dpdk utilties, i.e. testpmd and dpdk-pdump. It also sends IGMP 6 | requests (requires sudo) when a filter expression is given. 7 | Usage: 8 | $0 -i interface0 [-i interface1] -w file.pcap [-G ] [-v|V] [ filter expr ] 9 | -i Network interface(s), be aware that dpdk captures everything, ptp included. 10 | -w Output pcap file 11 | -G Capture duration 12 | -v Verbose 13 | -V Very verbose 14 | filter expression tcpdump-like expression, but only for multicast to be subscribed by IGMP 15 | 16 | Exples: 17 | $0 -i enp1s0f0 -w /tmp/single.pcap -G 1 dst 225.192.10.1 or dst 225.192.10.2 18 | $0 -V -i enp1s0f0 -i enp1s0f1 -w /tmp/dual.pcap -G 1 # dual port means that local ptp slave won't see ptp traffic 19 | " >&2 20 | } 21 | 22 | duration=2 23 | verbose=0 24 | dual_port=0 25 | testpmd_log=/tmp/dpdk-testpmd.log 26 | iface="" 27 | 28 | dpdk_log(){ 29 | echo "dpdk-capture: $@" 30 | } 31 | 32 | if ps aux | grep -q [p]dump; then 33 | dpdk_log "dpdk-pdump is already in use, exit." 34 | exit 2 35 | fi 36 | 37 | dpdk_log "Parse args: ------------------------------------------ " 38 | 39 | # typical cmdline to be translated: 40 | # $ tcpdump -i interfaceName --time-stamp-precision=nano \ 41 | # -j adapter_unsynced\--snapshot-length=N -v -w pcap -G 2 -W 1 \ 42 | # dst 192.168.1.1 or dst 192.168.1.2 43 | while getopts ":i:w:G:W:vV" o; do 44 | case "${o}" in 45 | i | interface) 46 | if [ ! -z "$iface" ]; then 47 | dual_port=1 48 | fi 49 | 50 | iface="$iface ${OPTARG}" 51 | ;; 52 | j) 53 | ;; 54 | #-) 55 | # case ${OPTARG} in 56 | # time-stamp-precision*) 57 | # ;; 58 | # snapshot-length*) 59 | # ;; 60 | # esac 61 | # ;; 62 | w) 63 | output=${OPTARG} 64 | ;; 65 | G) 66 | duration=${OPTARG} 67 | ;; 68 | W) 69 | #ignore file number 70 | ;; 71 | v) 72 | verbose=1 73 | ;; 74 | V) 75 | set -x 76 | verbose=1 77 | ;; 78 | *) 79 | dpdk_log "unsupported option ${o}" 80 | usage 81 | exit 1 82 | ;; 83 | esac 84 | done 85 | 86 | shift $((OPTIND-1)) 87 | 88 | if [ -z "$iface" -o -z "$output" ]; then 89 | dpdk_log "Missing argument" 90 | usage 91 | exit 1 92 | fi 93 | 94 | filter=$@ 95 | IPs=$(echo $filter | sed 's/dst//g; s/or//g' | tr -s ' ' '\n') 96 | pcap=$(dirname $output)/output 97 | 98 | dpdk_log " 99 | iface: $iface 100 | pcap: $output 101 | filter: $filter 102 | dual_port: $dual_port 103 | duration: $duration" 104 | 105 | 106 | dpdk_log "Checking interface: $i ------------------------------------------ " 107 | 108 | pci="" 109 | for i in $iface; do 110 | if [ ! -d /sys/class/net/$i ]; then 111 | dpdk_log "$i doesn\'t exist, exit." 112 | exit 1 113 | fi 114 | if [ $(cat /sys/class/net/$i/operstate) != "up" ]; then 115 | dpdk_log "$i is not up, exit." 116 | exit 1 117 | fi 118 | pci="$pci -w $(dpdk-devbind --status | grep "if=$i" | cut -d ' ' -f1)" 119 | done 120 | 121 | dpdk_log "Joining mcast: $IPs ------------------------------------------ " 122 | for i in $iface; do 123 | if [ ! -z "$filter" ]; then 124 | 125 | if ! smcroutectl show > /dev/null; then 126 | smcrouted 127 | fi 128 | 129 | for ip in $IPs; do 130 | smcroutectl join $i $ip 131 | if ! netstat -ng | grep -q "$i.*$ip"; then 132 | dpdk_log "Can\'t joint $ip" 133 | fi 134 | done 135 | 136 | if [ $verbose -eq 1 ]; then 137 | netstat -ng | grep $i 138 | fi 139 | else 140 | dpdk_log "No filter" 141 | fi 142 | done 143 | 144 | if [ $dual_port -eq 1 ]; then 145 | dpdk_log "Pausing PTP------------------------------------------" 146 | # prevent linuxptp from interfering with the timestamping 147 | /etc/init.d/ptp stop 148 | fi 149 | 150 | # dpdk 151 | dpdk_log "Capturing------------------------------------------" 152 | 153 | if ps aux | grep -q [t]estpmd; then 154 | dpdk_log "PMD already up" 155 | else 156 | dpdk_log "Start PMD" 157 | # create a detached session to run PMD server 158 | screen -dmS testpmd -L -Logfile $testpmd_log \ 159 | testpmd $pci -l 0-3 -n 4 -- --enable-rx-timestamp --forward-mode=rxonly 160 | 161 | sleep 3 162 | fi 163 | 164 | # filter out ptp 165 | # testpmd must be in --interactive 166 | #screen -S testpmd -X stuff "bpf-load rx 0 0 J /tmp/bpf_no_ptp.o 167 | #start 168 | #" 169 | 170 | #pkt_rx_start=$(ethtool -S $i | grep rx_packets: | sed 's/.*: \(.*\)/\1/') 171 | #pkt_drop_start=$(ethtool -S $i | grep rx_out_of_buffer: | sed 's/.*: \(.*\)/\1/') 172 | 173 | dpdk_log "Start pdump" 174 | if [ $dual_port -eq 1 ]; then 175 | args="-- --pdump port=0,queue=0,rx-dev=$pcap-0.pcap --pdump port=1,queue=0,rx-dev=$pcap-1.pcap" 176 | else 177 | port=$(echo $iface | sed 's/.*\(.\)/\1/') 178 | args="-- --pdump port=$port,queue=*,rx-dev=$pcap-$port.pcap" 179 | fi 180 | dpdk-pdump $args 2>&1 & 181 | 182 | sleep $duration 183 | 184 | dpdk_log "Stop testpmd / pdump -------------------------------------" 185 | # send a SGINT after after duration 186 | killall -s 2 dpdk-pdump 187 | 188 | # send carriage return to stop testpmd 189 | screen -S testpmd -X stuff " 190 | " 191 | if [ $verbose -eq 1 ]; then 192 | cat $testpmd_log 193 | fi 194 | rm $testpmd_log 195 | 196 | if [ $dual_port -eq 1 ]; then 197 | dpdk_log "Resuming PTP: $ptp_cmd -------------------------------------" 198 | /etc/init.d/ptp start 199 | fi 200 | 201 | for i in $iface; do 202 | if [ ! -z "$filter" ]; then 203 | dpdk_log "Leaving mcast ------------------------------------------" 204 | for ip in $IPs; do 205 | smcroutectl leave $i $ip 206 | done 207 | fi 208 | 209 | port=$(echo $i | sed 's/.*\(.\)/\1/') 210 | if [ ! -f $pcap-$port.pcap ]; then 211 | dpdk_log "File not found: $pcap-$port.pcap" 212 | exit 1 213 | fi 214 | 215 | if [ $verbose -eq 1 ]; then 216 | dpdk_log "pcapinfo port $port" 217 | capinfos $pcap-$port.pcap 218 | fi 219 | done 220 | 221 | #pkt_rx_end=$(ethtool -S $i | grep rx_packets: | sed 's/.*: \(.*\)/\1/') 222 | #pkt_drop_end=$(ethtool -S $i | grep rx_out_of_buffer: | sed 's/.*: \(.*\)/\1/') 223 | #dpdk_log "rx: $(echo "$pkt_rx_end - $pkt_rx_start" | bc)" 224 | #dpdk_log "drop: $(echo "$pkt_drop_end - $pkt_drop_start" | bc)" 225 | 226 | if [ $dual_port -eq 1 ]; then 227 | mergecap -w $output -F nsecpcap $pcap-0.pcap $pcap-1.pcap 228 | echo $(ls $pcap-[01].pcap) merged into $pcap.pcap 229 | rm -f $pcap-0.pcap $pcap-1.pcap 230 | else 231 | port=$(echo $iface | sed 's/.*\(.\)/\1/') 232 | mv $pcap-$port.pcap $output 233 | fi 234 | 235 | chmod 666 $output 236 | -------------------------------------------------------------------------------- /capture/dpdk/install.sh: -------------------------------------------------------------------------------- 1 | install_dpdk() 2 | { 3 | ln -sf $TOP_DIR/capture/dpdk/dpdk-capture.sh /usr/sbin/dpdk-capture.sh 4 | 5 | apt install -y libnuma-dev libelf-dev libpcap-dev 6 | 7 | echo "Installing dpdk" 8 | DIR=$(mktemp -d) 9 | cd $DIR/ 10 | git clone https://github.com/pkeroulas/dpdk.git 11 | cd dpdk 12 | git checkout -b clock_info origin/pdump_mlx5_hw_ts/clock_info/v1 13 | 14 | make defconfig 15 | sed -i 's/MLX5_PMD=.*/MLX5_PMD=y/' ./build/.config 16 | sed -i 's/MLX5_DEBUG=.*/MLX5_DEBUG=y/' ./build/.config 17 | sed -i 's/PMD_PCAP=.*/PMD_PCAP=y/' ./build/.config 18 | 19 | MAKE_PAUSE=n make -j2 20 | make install 21 | rm -rf $DIR 22 | 23 | for p in testpmd dpdk-pdump smcroutectl; do 24 | bin=$(readlink -f $(which $p)) 25 | chgrp pcap $bin 26 | setcap cap_net_raw,cap_net_admin=eip $bin 27 | done 28 | } 29 | -------------------------------------------------------------------------------- /capture/dpdk/libpcap-HW-TS-support-for-dpdk.patch: -------------------------------------------------------------------------------- 1 | From 12cd665d33b7a103bd98ad80cdbe3b4f9d66b51d Mon Sep 17 00:00:00 2001 2 | From: Patrick Keroulas 3 | Date: Tue, 21 Jul 2020 16:18:39 -0400 4 | Subject: dpdk: support HW timestamps 5 | 6 | --- 7 | pcap-dpdk.c | 92 ++++++++++++++++++++++++++++++++++++++++++----------- 8 | 1 file changed, 74 insertions(+), 18 deletions(-) 9 | 10 | diff --git a/pcap-dpdk.c b/pcap-dpdk.c 11 | index 837ad1c2..cc3ad0ef 100644 12 | --- a/pcap-dpdk.c 13 | +++ b/pcap-dpdk.c 14 | @@ -89,6 +89,10 @@ env DPDK_CFG="--log-level=debug -l0 -dlibrte_pmd_e1000.so -dlibrte_pmd_ixgbe.so 15 | 16 | #include 17 | 18 | +#ifdef HAVE_LINUX_NET_TSTAMP_H 19 | +#include 20 | +#endif 21 | + 22 | //header for calling dpdk 23 | #include 24 | #include 25 | @@ -168,6 +172,19 @@ static uint16_t nb_txd = RTE_TEST_TX_DESC_DEFAULT; 26 | #define RTE_ETH_PCAP_SNAPLEN ETHER_MAX_JUMBO_FRAME_LEN 27 | #endif 28 | 29 | +/* 30 | + * Map SOF_TIMESTAMPING_ values to PCAP_TSTAMP_ values. 31 | + */ 32 | +static const struct { 33 | + int soft_timestamping_val; 34 | + int pcap_tstamp_val; 35 | +} sof_ts_type_map[3] = { 36 | + { SOF_TIMESTAMPING_SOFTWARE, PCAP_TSTAMP_HOST }, 37 | + { SOF_TIMESTAMPING_SYS_HARDWARE, PCAP_TSTAMP_ADAPTER }, 38 | + { SOF_TIMESTAMPING_RAW_HARDWARE, PCAP_TSTAMP_ADAPTER_UNSYNCED } 39 | +}; 40 | +#define NUM_SOF_TIMESTAMPING_TYPES (sizeof sof_ts_type_map / sizeof sof_ts_type_map[0]) 41 | + 42 | static struct rte_eth_dev_tx_buffer *tx_buffer; 43 | 44 | struct dpdk_ts_helper{ 45 | @@ -253,23 +270,40 @@ static void dpdk_fmt_errmsg_for_rte_errno(char *errbuf, size_t errbuflen, 46 | } 47 | 48 | static int dpdk_init_timer(struct pcap_dpdk *pd){ 49 | + struct timeval now; 50 | + rte_eth_read_clock(pd->portid, &pd->ts_helper.start_cycles); 51 | + rte_eth_get_clock_freq(pd->portid, &pd->ts_helper.hz); 52 | + 53 | gettimeofday(&(pd->ts_helper.start_time),NULL); 54 | + /* 55 | pd->ts_helper.start_cycles = rte_get_timer_cycles(); 56 | pd->ts_helper.hz = rte_get_timer_hz(); 57 | if (pd->ts_helper.hz == 0){ 58 | return -1; 59 | } 60 | + */ 61 | return 0; 62 | } 63 | -static inline void calculate_timestamp(struct dpdk_ts_helper *helper,struct timeval *ts) 64 | +static int prout = 0; 65 | +static inline void calculate_timestamp(struct dpdk_ts_helper *helper,struct timeval *ts, struct rte_mbuf *pkt) 66 | { 67 | uint64_t cycles; 68 | // delta 69 | struct timeval cur_time; 70 | - cycles = rte_get_timer_cycles() - helper->start_cycles; 71 | - cur_time.tv_sec = (time_t)(cycles/helper->hz); 72 | - cur_time.tv_usec = (suseconds_t)((cycles%helper->hz)*1e6/helper->hz); 73 | - timeradd(&(helper->start_time), &cur_time, ts); 74 | + 75 | + if (pkt && (pkt->ol_flags & PKT_RX_TIMESTAMP)) { 76 | + uint64_t start_ts_ns = helper->start_time.tv_sec * 1000000000 + helper->start_time.tv_usec * 1000; 77 | + uint64_t ts_ns = start_ts_ns + 78 | + (pkt->timestamp - helper->start_cycles) * 1000000000 / helper->hz; 79 | + ts->tv_sec = ts_ns / 1000000000; 80 | + ts->tv_usec = ts_ns % 1000000000; 81 | + } 82 | + else { 83 | + cycles = rte_get_timer_cycles() - helper->start_cycles; 84 | + cur_time.tv_sec = (time_t)(cycles/helper->hz); 85 | + cur_time.tv_usec = (suseconds_t)((cycles%helper->hz)*1e6/helper->hz); 86 | + timeradd(&(helper->start_time), &cur_time, ts); 87 | + } 88 | } 89 | 90 | static uint32_t dpdk_gather_data(unsigned char *data, uint32_t len, struct rte_mbuf *mbuf) 91 | @@ -363,7 +397,7 @@ static int pcap_dpdk_dispatch(pcap_t *p, int max_cnt, pcap_handler cb, u_char *c 92 | pkt_cnt += nb_rx; 93 | for ( i = 0; i < nb_rx; i++) { 94 | m = pkts_burst[i]; 95 | - calculate_timestamp(&(pd->ts_helper),&(pcap_header.ts)); 96 | + calculate_timestamp(&(pd->ts_helper),&(pcap_header.ts), m); 97 | pkt_len = rte_pktmbuf_pkt_len(m); 98 | // caplen = min(pkt_len, p->snapshot); 99 | // caplen will not be changed, no matter how long the rte_pktmbuf 100 | @@ -447,7 +481,7 @@ static void nic_stats_display(struct pcap_dpdk *pd) 101 | static int pcap_dpdk_stats(pcap_t *p, struct pcap_stat *ps) 102 | { 103 | struct pcap_dpdk *pd = p->priv; 104 | - calculate_timestamp(&(pd->ts_helper), &(pd->curr_ts)); 105 | + calculate_timestamp(&(pd->ts_helper), &(pd->curr_ts), NULL); 106 | rte_eth_stats_get(pd->portid,&(pd->curr_stats)); 107 | if (ps){ 108 | ps->ps_recv = pd->curr_stats.ipackets; 109 | @@ -771,16 +805,6 @@ static int pcap_dpdk_activate(pcap_t *p) 110 | return PCAP_ERROR_NO_SUCH_DEVICE; 111 | } 112 | 113 | - ret = dpdk_init_timer(pd); 114 | - if (ret<0) 115 | - { 116 | - snprintf(p->errbuf, PCAP_ERRBUF_SIZE, 117 | - "dpdk error: Init timer is zero with device %s", 118 | - p->opt.device); 119 | - ret = PCAP_ERROR; 120 | - break; 121 | - } 122 | - 123 | nb_ports = rte_eth_dev_count_avail(); 124 | if (nb_ports == 0) 125 | { 126 | @@ -801,6 +825,16 @@ static int pcap_dpdk_activate(pcap_t *p) 127 | 128 | pd->portid = portid; 129 | 130 | + ret = dpdk_init_timer(pd); 131 | + if (ret<0) 132 | + { 133 | + snprintf(p->errbuf, PCAP_ERRBUF_SIZE, 134 | + "dpdk error: Init timer is zero with device %s", 135 | + p->opt.device); 136 | + ret = PCAP_ERROR; 137 | + break; 138 | + } 139 | + 140 | if (p->snapshot <= 0 || p->snapshot > MAXIMUM_SNAPLEN) 141 | { 142 | p->snapshot = MAXIMUM_SNAPLEN; 143 | @@ -823,6 +857,9 @@ static int pcap_dpdk_activate(pcap_t *p) 144 | { 145 | local_port_conf.txmode.offloads |=DEV_TX_OFFLOAD_MBUF_FAST_FREE; 146 | } 147 | + 148 | + local_port_conf.rxmode.offloads |= DEV_RX_OFFLOAD_TIMESTAMP; 149 | + 150 | // only support 1 queue 151 | ret = rte_eth_dev_configure(portid, 1, 1, &local_port_conf); 152 | if (ret < 0) 153 | @@ -919,7 +956,7 @@ static int pcap_dpdk_activate(pcap_t *p) 154 | } 155 | // reset statistics 156 | rte_eth_stats_reset(pd->portid); 157 | - calculate_timestamp(&(pd->ts_helper), &(pd->prev_ts)); 158 | + calculate_timestamp(&(pd->ts_helper), &(pd->prev_ts), NULL); 159 | rte_eth_stats_get(pd->portid,&(pd->prev_stats)); 160 | // format pcap_t 161 | pd->portid = portid; 162 | @@ -974,6 +1011,25 @@ pcap_t * pcap_dpdk_create(const char *device, char *ebuf, int *is_ours) 163 | //memset will happen 164 | p = PCAP_CREATE_COMMON(ebuf, struct pcap_dpdk); 165 | 166 | + // timestamps feature 167 | + p->tstamp_precision_count = 2; 168 | + p->tstamp_precision_list = malloc(2 * sizeof(u_int)); 169 | + if (p->tstamp_precision_list == NULL) { 170 | + pcap_fmt_errmsg_for_errno(ebuf, PCAP_ERRBUF_SIZE, 171 | + errno, "malloc"); 172 | + pcap_close(p); 173 | + return NULL; 174 | + } 175 | + p->tstamp_precision_list[0] = PCAP_TSTAMP_PRECISION_MICRO; 176 | + p->tstamp_precision_list[1] = PCAP_TSTAMP_PRECISION_NANO; 177 | + 178 | + u_int i; 179 | + 180 | + p->tstamp_type_count = NUM_SOF_TIMESTAMPING_TYPES; 181 | + p->tstamp_type_list = malloc(NUM_SOF_TIMESTAMPING_TYPES * sizeof(u_int)); 182 | + for (i = 0; i < NUM_SOF_TIMESTAMPING_TYPES; i++) 183 | + p->tstamp_type_list[i] = sof_ts_type_map[i].pcap_tstamp_val; 184 | + 185 | if (p == NULL) 186 | return NULL; 187 | p->activate_op = pcap_dpdk_activate; 188 | -- 189 | 2.17.1 190 | 191 | -------------------------------------------------------------------------------- /capture/get_sdp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | 3 | import sys 4 | import json 5 | import urllib2 6 | import re 7 | 8 | def usage(): 9 | print(""" 10 | get_sdp.py - helper script to fetch SDP file from Embrionix sender and 11 | \tselect multiple flows and pack then in a single SDP (illegal in ST 2110). 12 | 13 | Usage: 14 | \tget_sdp.py [flow indexes] 15 | 16 | examples: 17 | \t$ ./get_sdp.py 192.168.1.10 # keep the flows from 1 SDI input: 0-19 18 | \t$ ./get_sdp.py 192.168.1.10 0 2 18 # typically 1st video, 1st audio and 1st anc 19 | 20 | See flow mapping in ../doc/embrionix.md 21 | """) 22 | 23 | def get_sdp_url(ip): 24 | return "http://" + ip + "/emsfp/node/v1/sdp/" 25 | 26 | def get_from_url(url): 27 | try: 28 | sdp = urllib2.urlopen(url, timeout=1).read() 29 | except urllib2.HTTPError: 30 | print("Unable to fetch SDP") 31 | 32 | return sdp 33 | 34 | def write_sdp_file(sdp): 35 | # get last digit of sender IP 36 | lines = re.findall(r'o=.*', sdp) 37 | source_ip = re.findall(r'[0-9]+(?:\.[0-9]+){3}', lines[0])[0] 38 | filename="emb_encap_" + source_ip.split(".")[3] + ".sdp" 39 | 40 | file = open(filename, 'w') 41 | file.write(sdp) 42 | file.close() 43 | 44 | print("-" * 72) 45 | print("SDP written to " + filename) 46 | 47 | def main(): 48 | if len(sys.argv) < 2: 49 | usage() 50 | return 51 | 52 | ip_address = sys.argv[1] 53 | url = get_sdp_url(ip_address) 54 | content = get_from_url(url) 55 | 56 | sdp_list = "" 57 | try: 58 | sdp_list = json.loads(content) 59 | except: 60 | print("Unable to parse json") 61 | return 62 | 63 | if sys.argv[2:]: 64 | flow_indexes = [int(i) for i in sys.argv[2:]]; 65 | else: 66 | flow_indexes = [i for i in range(20)]; 67 | 68 | print("-" * 72) 69 | print("Go fetch flows: {}".format(flow_indexes)) 70 | sdp_filtered="" 71 | got_description = False 72 | for i in flow_indexes: 73 | url = get_sdp_url(ip_address) + str(sdp_list[i]) 74 | sdp = str(get_from_url(url)) + '\n' 75 | 76 | if not got_description: 77 | # 1st flow: keep description but add a separator 78 | expr = re.compile(r'(^t=.*\n)', re.MULTILINE) 79 | sdp = re.sub(expr, r'\1\n', sdp) 80 | else: 81 | # other flows: skip description 82 | expr = re.compile(r'^o=.*\n|^v=.*\n|s=.*\n|t=.*\n', re.MULTILINE) 83 | sdp = re.sub(expr, '', sdp) 84 | 85 | print("-" * 72) 86 | print("Flow:{}\n{}".format(i,sdp)) 87 | sdp_filtered += sdp 88 | got_description = True 89 | 90 | write_sdp_file(sdp_filtered) 91 | 92 | if __name__ == "__main__": 93 | main() 94 | -------------------------------------------------------------------------------- /capture/install.sh: -------------------------------------------------------------------------------- 1 | # !!! Don't execute this script directly !!! 2 | # It is imported in $TOP/install.sh 3 | 4 | export LANG=en_US.utf8 \ 5 | SMCROUTE_VERSION=2.4.3 6 | 7 | source $TOP_DIR/capture/dpdk/install.sh 8 | 9 | install_mellanox() 10 | { 11 | echo "Installing Mellanox libs" 12 | 13 | apt install -y rdma-core libibverbs-dev 14 | 15 | # MFT: Mellanox Firmware Tools 16 | # https://network.nvidia.com/products/adapter-software/firmware-tools/ 17 | # It includes mst utility but it turns out it is not necessaery for 18 | # capture 19 | # 20 | # install: 21 | # apt install -y dkms 22 | # DIR=$(mktemp -d) 23 | # cd $DIR/ 24 | # wget http://www.mellanox.com/downloads/MFT/mft-4.5.0-31-x86_64-rpm.tgz 25 | # tar xzvf mft-4.5.0-31-x86_64-rpm.tgz 26 | # cd mft-4.5.0-31-x86_64-rpm 27 | # ./install.sh 28 | # rm -rf $DIR 29 | } 30 | 31 | install_smcroute() 32 | { 33 | echo "Installing smcroute" 34 | DIR=$(mktemp -d) 35 | cd $DIR/ 36 | wget https://github.com/troglobit/smcroute/releases/download/2.4.3/smcroute-$SMCROUTE_VERSION.tar.gz 37 | tar xaf smcroute-$SMCROUTE_VERSION.tar.gz 38 | cd smcroute-$SMCROUTE_VERSION 39 | ./autogen.sh 40 | ./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var 41 | make 42 | make install 43 | make distclean 44 | rm -rf $DIR 45 | 46 | bin=$(readlink -f $(which smcroutectl)) 47 | chgrp pcap $bin 48 | setcap cap_net_raw,cap_net_admin=eip $bin 49 | } 50 | 51 | install_capture() 52 | { 53 | install_mellanox 54 | install_dpdk 55 | install_smcroute 56 | } 57 | -------------------------------------------------------------------------------- /capture/network_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT=$(basename $0) 4 | ST2110_CONF_FILE=/etc/st2110.conf 5 | 6 | usage (){ 7 | echo -e "$SCRIPT create routes to accept multicast traffic on a 8 | given interface. 9 | 10 | \t$SCRIPT " 11 | } 12 | 13 | # network functions 14 | mask2cdr () 15 | { 16 | # Assumes there's no "255." after a non-255 byte in the mask 17 | local x=${1##*255.} 18 | set -- 0^^^128^192^224^240^248^252^254^ $(( (${#1} - ${#x})*2 )) ${x%%.*} 19 | x=${1%%$3*} 20 | echo $(( $2 + (${#x}/4) )) 21 | } 22 | 23 | subnet () 24 | { 25 | ip=$1 26 | mask=$2 27 | IFS=. read -r i1 i2 i3 i4 <<< $ip 28 | IFS=. read -r m1 m2 m3 m4 <<< $mask 29 | printf "%d.%d.%d.%d\n" "$((i1 & m1))" "$((i2 & m2))" "$((i3 & m3))" "$((i4 & m4))" 30 | } 31 | 32 | cdr2mask () 33 | { 34 | # Number of args to shift, 255..255, first non-255 byte, zeroes 35 | set -- $(( 5 - ($1 / 8) )) 255 255 255 255 $(( (255 << (8 - ($1 % 8))) & 255 )) 0 0 0 36 | [ $1 -gt 1 ] && shift $1 || shift 37 | echo ${1-0}.${2-0}.${3-0}.${4-0} 38 | } 39 | 40 | if [ $# -ne 1 ]; then 41 | usage 42 | exit 1 43 | fi 44 | sdp_file=$1 45 | 46 | if [ ! -f $ST2110_CONF_FILE ]; then 47 | echo "Couldn't find conf file $ST2110_CONF_FILE" 48 | exit 1 49 | fi 50 | source $ST2110_CONF_FILE 51 | 52 | echo "-------------------------------------------" 53 | echo "Local info:" 54 | # join multicast groups through the media gateway 55 | ipaddr=$(ip addr show $MEDIA_IFACE | sed -n "s/.*inet \(.*\)\/.*/\1/p") 56 | cidr=$(ip addr show $MEDIA_IFACE | sed -n 's/.*inet .*\/\(.*\) brd.*/\1/p') 57 | gateway=$(ip route | sed -n 's/default via \(.*\) dev '""$(echo $MEDIA_IFACE | tr -d '\n')""' .*/\1/p') 58 | netmask=$(cdr2mask $cidr) 59 | subnet=$(subnet $ipaddr $netmask) 60 | 61 | echo "Address: $ipaddr 62 | Gateway: $gateway 63 | Netmask: $netmask 64 | Subnet: $subnet" 65 | 66 | if [ -z $gateway -o -z $ipaddr -o -z $subnet ]; then 67 | echo "Missing network info, exit." 68 | exit 1 69 | fi 70 | 71 | # The control unicast IP of the source should be accessible through the 72 | # gateway. This MAY be necessary for the reverse path resolution of the source in 73 | # order to accept traffic. 74 | source_ip="$(sed -n 's/^o=.*IN IP4 \(.*\)$/\1/p' $sdp_file | sed 's/\r//')" 75 | 76 | # Get media source IP instead 77 | # source_ip="$(sed -n 's/^a=source-filter.*IN IP4 .* \(.*\)$/\1/p' $sdp_file | sed 's/\r//' | uniq)" 78 | 79 | multicast_groups=$(sed -n 's/^c=IN IP4 \(.*\)\/.*/\1/p' $sdp_file) 80 | # get the port of the last essence which is associated to the last 81 | # multicast group, i.e. $gr 82 | port=$(sed -n 's/^m=.* \(.*\) RTP.*/\1/p' $sdp_file | tail -1) 83 | 84 | echo "-------------------------------------------" 85 | echo "Source/Sender:" 86 | echo "Address: $source_ip 87 | Multicast Groups: " $multicast_groups 88 | 89 | if [ -z "$source_ip" -o -z "$multicast_groups" ]; then 90 | echo "Missing info in $sdp_file" 91 | exit 1 92 | fi 93 | 94 | # ip route add $subnet/$cidr via $gateway dev $MEDIA_IFACE 95 | if ! ping -W 1 -I $MEDIA_IFACE -c 1 -q $source_ip > /dev/null; then 96 | echo "Couln't ping source @ $source_ip, add a route to source" 97 | ip route add $source_ip via $gateway dev $MEDIA_IFACE 98 | 99 | # disable reverse path filtering, useless if explicit route is added" 100 | # sysctl -w net.ipv4.conf.all.rp_filter=0 101 | # sysctl -w net.ipv4.conf.$MEDIA_IFACE.rp_filter=0 102 | 103 | if ! ping -W 1 -I $MEDIA_IFACE -c 1 -q $source_ip > /dev/null; then 104 | echo "Couln't ping source @ $source_ip, exit." 105 | exit 1 106 | fi 107 | fi 108 | 109 | for gr in $multicast_groups;do 110 | if ! ip route | grep -q "$gr dev $MEDIA_IFACE scope link"; then 111 | echo "Add route for $gr" 112 | ip route add $gr dev $MEDIA_IFACE 113 | else 114 | echo "Route for $gr already exists." 115 | fi 116 | done 117 | 118 | echo "-------------------------------------------" 119 | echo "Firewall:" 120 | if iptables -C INPUT -d 224.0.0.0/4 -j ACCEPT 2> /dev/null 121 | then 122 | echo "iptable rule for multicast rule already exists" 123 | else 124 | iptables -I INPUT 1 -d 224.0.0.0/4 -j ACCEPT 125 | echo "Add a rule in firewall to allow incoming multicast" 126 | fi 127 | 128 | echo "-------------------------------------------" 129 | echo "Rx test:" 130 | 131 | dumpfile=/tmp/dump.log 132 | socat -u UDP4-RECV:$port,ip-add-membership=$gr:$MEDIA_IFACE $dumpfile & 133 | pid=$! 134 | sleep 1 135 | kill $pid 136 | 137 | if [ ! -f $dumpfile ]; then 138 | echo "No data received." 139 | exit 1 140 | fi 141 | 142 | size=$(stat $dumpfile | sed -n 's/.*Size: \(.*\)\tBlocks:.*/\1/p') 143 | echo "Received $size bytes in 1 sec" 144 | rm -rf $dumpfile 145 | 146 | exit 0 147 | -------------------------------------------------------------------------------- /capture/nic_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SCRIPT=$(basename $0) 3 | ST2110_CONF_FILE=/etc/st2110.conf 4 | 5 | usage (){ 6 | echo -e "$SCRIPT configure the NIC of the media port for optimization. 7 | The user must have privileged rights. 8 | 9 | \t$SCRIPT " 10 | 11 | } 12 | 13 | if [ $# -ne 1 ]; then 14 | usage 15 | exit 1 16 | fi 17 | 18 | media_iface=$1 19 | 20 | echo "-------------------------------------------" 21 | echo "Check interface:" 22 | 23 | if [ ! -d /sys/class/net/$media_iface ]; then 24 | echo "$media_iface is not a network interface." 25 | exit 1 26 | fi 27 | 28 | if [ $(cat /sys/class/net/$media_iface/operstate) != "up" ]; then 29 | echo "$media_iface is not up, exit." 30 | exit 1 31 | fi 32 | 33 | echo "$media_iface: OK" 34 | 35 | # save interface in the config 36 | if [ ! -f $ST2110_CONF_FILE ]; then 37 | echo "MEDIA_IFACE=$media_iface" > $ST2110_CONF_FILE 38 | elif grep -q "MEDIA_IFACE=.*" $ST2110_CONF_FILE; then 39 | sed -i 's/\(MEDIA_IFACE=\).*/\1'$media_iface'/' $ST2110_CONF_FILE 40 | else 41 | echo "MEDIA_IFACE=$media_iface" >> $ST2110_CONF_FILE 42 | fi 43 | 44 | 45 | echo "-------------------------------------------" 46 | echo "Setup input buffer:" 47 | buffer_size=671088640 48 | sysctl net.core.rmem_max=$buffer_size 49 | sysctl net.core.rmem_default=$buffer_size 50 | 51 | echo "-------------------------------------------" 52 | echo "Setup interface $media_iface:" 53 | ethtool -G $media_iface rx 4096 # ring buffer 54 | ethtool -K $media_iface rx off # don't compute checksum 55 | ethtool -C $media_iface rx-usecs 48 # coalescence: interrupt moderation 56 | 57 | exit 0 58 | -------------------------------------------------------------------------------- /capture/rtcpdump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | usage(){ 4 | echo " 5 | 'rtcpdump' starts 'tcpdump' on a remote host (Linux or Arista switch), 6 | opens 'wireshark' locally and pipes them together to display the distant 7 | network packets in a GUI, in realtime. 8 | 9 | This solution makes the troubleshoot of (lightweigth) network protocols 10 | faster and more confortable, without being physically connected to the 11 | spotted network segment. 12 | 13 | Usage: 14 | $0 -r @ -p -i 15 | [-P user@proxy] [-c ] [-v] [-d rx|tx|both] [-a] ['filter_expression'] 16 | 17 | -r ssh path; can be an alias in your local ssh config 18 | -P ssh proxy proxy; can also be an alias 19 | -p password used if you have sshpass installed, can be a password file 20 | -i remote interface name. On a switch, it can either be simple '10' 21 | or a part of a quad-port '10/2' 22 | -c limit of captured packets (default $pkt_count as safety for network) 23 | -C capture cpu-targeted protocols (PTP, IGMP, ARP...) directly on a CPU iface, no port mirroring 24 | -v verbose 25 | -d direction 'rx|tx': keep ingress traffic only or egress. Default is both 26 | -a Arista ACL filter mode must be followed by an ACL rule (rx only) 27 | filter_expression: tcpdump-like expression (ACL-like if '-a ...') with additional aliases 28 | supported see example below. 29 | 30 | Examples: 31 | - PTP on Arista switch port, directly on the CPU: 32 | $0 -r user@server -p pass -C -i Et10/1 ptp 33 | - IGMP using ACL mode, with password file provided and verbose mode: 34 | $0 -r user@server -p ~/passwordfile.txt -i Et10/1 -v -a 'permit igmp any any' 35 | - LLDP: 36 | $0 -r user@server -p pass -i Et10/1 'ether proto 0x88CC' 37 | - HTTP between a Arista sw (on the management interface) and a specific host: 38 | $0 -r user@server -p pass -i Ma1 'port 80 and host XXX.XXX.XXX.XXX' 39 | - DHCP/bootp on a Linux host for a given MAC: 40 | $0 -r user@server -p pass -i ens192 'dhcp and ether host XX:XX:XX:XX:XX:XX' 41 | - VLAN-tagged http packets 42 | $0 -r user@server -p pass -i ens192 '\-e \(vlan 1434 and port 80\)' 43 | 44 | Script execution steps: 45 | - login to remote through ssh 46 | - detect if remote is normal linux host or Arista switch 47 | - if Arista, init a monitor session that mirrors targeted port to 48 | cpu interface 49 | - launch tcpdump in remote bash and output to stdout (raw) 50 | - launch local wireshark and read from stdin 51 | - clean up monitor session on wireshark exit 52 | 53 | Tested: 54 | - Localhost: Linux, Windows (WSL2 installed) 55 | - Arista switches (EOS-4.29.3M): DCS-7060SX2-48YC6, DCS-7280CR2A-30 DCS-7280SR2-48YC6, 56 | DCS-7280TR-48C6, DCS-7280CR3K-32D4, DCS-7020TR-48 57 | - Not supported: Arista sw like CCS-720XP-48ZC2, CCS-720XP-48Y6, DCS-7050SX-64 are not 58 | supported since 'Monitor session' is limited. Traffic-recirculation feature may reorder 59 | packets when combining rx and tx which results in an unrielable capture. 60 | 61 | Limitations: 62 | - capturing a high bitrate port isn't a good idea given the additional load transfer over the 63 | network. This is why the capture is limited to 10000 pkts by default. Additionally, a monitor 64 | session on an Arista switch consists in mirroring the traffic to the Cpu through a 10Mbps link. 65 | As a result, some packets may be lost, even when a filter is given to tcpdump. Consider using 66 | the '-C' or '-a' flags 67 | - note that 'StrictHostKeyChecking=no' option is used for ssh, at you own risks 68 | " 1>&2 69 | } 70 | 71 | function title() { 72 | printf "\e[1;34m====================================================\e[m\n" "$1" 73 | printf "\e[1;34m%s\e[m\n" "$1" 74 | } 75 | 76 | function warning() { 77 | printf "\e[1;33m%s\e[m\n" "$1" 78 | } 79 | 80 | function error() { 81 | printf "\e[1;31m%s\e[m\n" "$1" 82 | } 83 | ################################################################## 84 | # CONST 85 | 86 | pkt_count=10000 87 | session=RTCPDUMP 88 | filter_mode=tcpdump 89 | direction="both" 90 | proxy='' 91 | port_mirroring=true 92 | 93 | ################################################################## 94 | # PARSE ARGS 95 | 96 | while getopts ":r:p:P:i:c:Cd:va" o; do 97 | case "${o}" in 98 | r) 99 | remote=${OPTARG} 100 | ;; 101 | i) 102 | iface=${OPTARG} 103 | ;; 104 | p) 105 | if [ -f ${OPTARG} ]; then 106 | passfile=${OPTARG} 107 | else 108 | password=${OPTARG} 109 | fi 110 | ;; 111 | P) 112 | proxy="-o ProxyJump=${OPTARG}" 113 | ;; 114 | c) 115 | pkt_count=${OPTARG} 116 | ;; 117 | C) 118 | port_mirroring=false 119 | ;; 120 | d) 121 | direction=${OPTARG} 122 | ;; 123 | a) 124 | filter_mode=acl 125 | ;; 126 | v) 127 | set -x 128 | ;; 129 | *) 130 | error "unsupported option ${o}" 131 | usage 132 | exit 1 133 | ;; 134 | esac 135 | done 136 | 137 | shift $((OPTIND-1)) 138 | 139 | if [ -z "$remote" -o -z "$iface" ]; then 140 | error "Missing argument -r or -i" 141 | usage 142 | exit 1 143 | fi 144 | 145 | if [ ! "$direction" = "rx" -a ! "$direction" = "tx" -a ! "$direction" = "both" ]; then 146 | error "Wrong value for -d param" 147 | usage 148 | exit 1 149 | fi 150 | 151 | # filter: convert aliases to tcpdump-compatible expression 152 | filter=$@ 153 | filter=$(echo $filter | sed "s/ptp/\\\(dst port 319 or dst port 320\\\)/") 154 | filter=$(echo $filter | sed "s/dhcp/\\\(port 67 or port 68\\\)/") 155 | filter=$(echo $filter | sed "s/http/\\\(port 80 or port 443\\\)/") 156 | 157 | ssh_cmd="ssh -T -o StrictHostKeyChecking=no $proxy $remote " 158 | 159 | ################################################################## 160 | # CHECKS 161 | 162 | title "RTCPDUMP" 163 | 164 | if mount | grep -q "^C:\\\ on"; then 165 | echo "Host: WSL" 166 | wireshark="/mnt/c/Progra~1/Wireshark/Wireshark.exe" 167 | # wireshark may complain about IOR.txt wrong permission but the capture works fine 168 | else 169 | echo "Host: Linux" 170 | wireshark=$(which wireshark) 171 | fi 172 | 173 | if [ ! -f "$wireshark" ]; then 174 | error "$wireshark not found" 175 | exit 1 176 | fi 177 | 178 | if ! which ssh >/dev/null; then 179 | error "ssh client not found" 180 | exit 1 181 | fi 182 | 183 | echo "ssh cmd:" 184 | echo $ssh_cmd 185 | 186 | if which sshpass >/dev/null; then 187 | if [ ! -z "$passfile" ]; then 188 | pass="-f $passfile" 189 | elif [ ! -z "$password" ]; then 190 | pass="-p $password" 191 | else #from stdin 192 | pass="" 193 | fi 194 | ssh_cmd="sshpass $pass $ssh_cmd" 195 | else 196 | warning " 197 | sshpass not installed. It is going to be painful to enter the ssh 198 | password at multiple times. Do you still want to proceed? [y/n]" 199 | read no 200 | if [ $no = "n" ]; then 201 | exit 0 202 | fi 203 | fi 204 | 205 | ################################################################## 206 | # Remote detection = regular Linux (easy) 207 | 208 | if $ssh_cmd "ls" > /dev/null; then 209 | echo "Remote: Linux host" 210 | echo "Interfaces." 211 | ifaces=$($ssh_cmd "ls /sys/class/net") 212 | if echo $ifaces | grep -v -q $iface; then 213 | echo $iface not found 214 | exit 1 215 | fi 216 | title "Capture $cpu_iface." 217 | warning ">>>>>>>>>>> Press CTRL+C to interrupt. <<<<<<<<<<<<<" 218 | $ssh_cmd "tcpdump -i $iface -c $pkt_count -U -s0 -w - $filter" | "$wireshark" -k -i - 219 | exit 0 220 | fi 221 | 222 | ################################################################## 223 | # Remote detection = Arista 224 | 225 | echo "Remote: Arista switch" 226 | 227 | title "Connection: ssh " 228 | if ! $ssh_cmd "enable"; then 229 | echo "Can't connect to target with this cmd." 230 | echo "\"$ssh_cmd\"" 231 | echo "Exit." 232 | exit 1 233 | fi 234 | echo OK 235 | 236 | title "Interface: $iface" 237 | echo "lldp:" 238 | if ! $ssh_cmd "show lldp neighbors $iface"; then 239 | echo "Wrong interface ($iface)? Exit." 240 | exit 1 241 | fi 242 | 243 | echo "stats:" 244 | port_stat=$($ssh_cmd "show interfaces $iface") 245 | echo "$port_stat" 246 | if echo $port_stat | grep -q -v "is up"; then 247 | error "Port $iface is wrong or down, exit." 248 | exit 1 249 | fi 250 | 251 | acl_monitor_option="" 252 | if [ $filter_mode = "acl" ]; then 253 | title "Create a IP access list: $filter" 254 | $ssh_cmd "enable 255 | conf 256 | ip access-list $session 257 | $filter 258 | " 259 | # need a short break for Cpu iface allocation 260 | $ssh_cmd "enable 261 | show ip access-list $session 262 | " 263 | filter="" 264 | direction="rx" 265 | acl_monitor_option="ip access-group $session" 266 | fi 267 | 268 | if $port_mirroring; then 269 | title "Create a monitor session:" 270 | $ssh_cmd "enable 271 | conf 272 | monitor session $session source $iface $direction $acl_monitor_option 273 | monitor session $session destination Cpu" 274 | sleep 1 # need a short break for Cpu iface allocation 275 | sessions=$($ssh_cmd "enable 276 | show monitor session") 277 | echo "$sessions" | grep -v -e '^$' | grep -v "\-\-\-\-\-\-\-" 278 | cpu_iface=$(echo "$sessions" | grep $session -A 14 | grep Cpu | sed 's/.*(\(.*\))/\1/') 279 | 280 | if [ -z $cpu_iface ]; then 281 | echo "Couldn't find cpu interface. Exit." 282 | exit -1 283 | fi 284 | else 285 | # convert iface name from EOS to Linux (Et...10/1 => et10_1) 286 | cpu_iface=$(echo $iface | tr '[:upper:]' '[:lower:]' | sed 's/et[a-z]*\([0-9].*\)/et\1/;s/\//_/') 287 | fi 288 | 289 | title "Capture on CPU interface: $cpu_iface" 290 | tcpdump_cmd="tcpdump -i $cpu_iface -c $pkt_count -U -s0 -w - $filter" 291 | echo $tcpdump_cmd 292 | 293 | warning ">>>>>>>>>>> Press CTRL+C to interrupt. <<<<<<<<<<<<<" 294 | trap 'echo Interrupted.' SIGINT # catch Ctrl-C to exit Arista bash properly 295 | 296 | $ssh_cmd "enable 297 | conf 298 | bash $tcpdump_cmd" | "$wireshark" -k -i - 299 | 300 | title "Cleanup." 301 | if $port_mirroring; then 302 | $ssh_cmd "enable 303 | conf 304 | no ip access-list $session 305 | no monitor session $session 306 | show monitor session 307 | bash pidof tcpdump > /dev/null && killall tcpdump 308 | " | grep -v -e '^$' 309 | else 310 | $ssh_cmd "bash pidof tcpdump > /dev/null && killall tcpdump 311 | " | grep -v -e '^$' 312 | fi 313 | 314 | echo "Exit." 315 | -------------------------------------------------------------------------------- /config/bashrc: -------------------------------------------------------------------------------- 1 | echo "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%" 2 | echo "% ST2110 CONFIG %" 3 | echo "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%" 4 | 5 | st2110_conf=/etc/st2110.conf 6 | 7 | if [ -f $st2110_conf ]; then 8 | cat $st2110_conf 9 | source $st2110_conf 10 | export $(grep -v "^#" $st2110_conf | cut -d= -f1) 11 | else 12 | echo "Missing $st2110_conf" 13 | fi 14 | -------------------------------------------------------------------------------- /config/nginx.conf: -------------------------------------------------------------------------------- 1 | load_module "modules/ngx_rtmp_module.so"; 2 | 3 | worker_processes auto; 4 | events { 5 | worker_connections 1024; 6 | } 7 | 8 | # RTMP configuration 9 | rtmp { 10 | # ffmpeg output arg: "-f flv rtmp://localhost:1935/show/movie" 11 | server { 12 | listen 1935; # Listen on standard RTMP port 13 | chunk_size 4000; 14 | 15 | application show { 16 | live on; 17 | # Turn on HLS 18 | hls on; 19 | hls_path /tmp/hls/; 20 | hls_fragment 3; 21 | hls_playlist_length 60; 22 | # disable consuming the stream from nginx as rtmp 23 | deny play all; 24 | } 25 | } 26 | } 27 | 28 | http { 29 | sendfile off; 30 | tcp_nopush on; 31 | directio 512; 32 | default_type application/octet-stream; 33 | 34 | server { 35 | listen 80; 36 | 37 | location / { 38 | # Disable cache 39 | add_header 'Cache-Control' 'no-cache'; 40 | 41 | # CORS setup 42 | add_header 'Access-Control-Allow-Origin' '*' always; 43 | add_header 'Access-Control-Expose-Headers' 'Content-Length'; 44 | 45 | # allow CORS preflight requests 46 | if ($request_method = 'OPTIONS') { 47 | add_header 'Access-Control-Allow-Origin' '*'; 48 | add_header 'Access-Control-Max-Age' 1728000; 49 | add_header 'Content-Type' 'text/plain charset=UTF-8'; 50 | add_header 'Content-Length' 0; 51 | return 204; 52 | } 53 | 54 | types { 55 | application/dash+xml mpd; 56 | application/vnd.apple.mpegurl m3u8; 57 | video/mp2t ts; 58 | } 59 | 60 | root /tmp/hls/; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /config/st2110.bashrc: -------------------------------------------------------------------------------- 1 | echo "-----------------------------------------------" 2 | echo " SMPTE ST 2110 TOOLKIT SERVER " 3 | echo "-----------------------------------------------" 4 | 5 | printf "%-30.30s: %s\n" "Master init script:" "/etc/init.d/st2110" 6 | printf "%-30.30s: %s\n" "Network config:" "/etc/netplan/*" 7 | printf "%-30.30s: %s\n" "Network reload cmd:" "$ sudo netplan apply" 8 | 9 | st2110_conf=/etc/st2110.conf 10 | if [ -f $st2110_conf ]; then 11 | source $st2110_conf 12 | export $(grep -v "^#" $st2110_conf | cut -d= -f1) 13 | printf "%-30.30s: %s\n" "Master ST 2110 config file:" "$st2110_conf" 14 | else 15 | printf "%-30.30s: %s\n" "Master ST 2110 config file:" "$st2110_conf(missing)" 16 | fi 17 | 18 | printf "%-30.30s: %s\n" "EBU-LIST control script:" "ebu_list_ctl" 19 | echo 20 | echo "-----------------------------------------------" 21 | ebu_list_ctl show_usage 22 | echo 23 | ebu_list_ctl status 24 | -------------------------------------------------------------------------------- /config/st2110.conf: -------------------------------------------------------------------------------- 1 | # Conf file for ST2110 capture and transcoding 2 | 3 | #---------------------------------------- 4 | # Mandatory: 5 | #---------------------------------------- 6 | 7 | MGMT_IFACE=eth1 8 | 9 | MEDIA_IFACE_0=eth0 10 | MEDIA_IFACE_1=eth1 11 | MEDIA_IFACE=$MEDIA_IFACE_0 12 | 13 | PTP_IFACE_0=eth0 # ptp4l 14 | PTP_IFACE_1=eth1 # phc2sys 15 | 16 | # unix user 17 | ST2110_USER=ebulist 18 | # EBU LIST source directory 19 | LIST_PATH=/home/$ST2110_USER/pi-list/ 20 | # where ebu-list stores pcpa, analysis, raw media files etc. 21 | LIST_DATA_FOLDER=/home/$ST2110_USER/data 22 | # dev/prod profile: determine if server and ui run in container (prod) 23 | # or are built from sources (dev) 24 | LIST_DEV=false 25 | LIST_GUI=2 # version 1 or 2 26 | -------------------------------------------------------------------------------- /config/st2110.init: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | ### BEGIN INIT INFO 3 | # Provides: st2110 4 | # Required-Start: $time $network $local_fs $syslog 5 | # Required-Stop: 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 8 | # Short-Description: Start st2110-related services: setup media interfaces and mcast subscription daemon 9 | ### END INIT INFO 10 | # This header allows systemd to create a service. 11 | 12 | # To enable the initscript on SYSV init system: 13 | # Copy to /etc/init.d/st2110 with root ownership 14 | # $ update-rc.d st2110 defaults 15 | # $ systemctl enable st2110 16 | # $ systemctl start st2110 17 | 18 | log_st2110() 19 | { 20 | logger -t st2110 "$@" 21 | } 22 | 23 | ST2110_CONF_FILE=/etc/st2110.conf 24 | if [ -f $ST2110_CONF_FILE ]; then 25 | . $ST2110_CONF_FILE 26 | fi 27 | 28 | log_list() 29 | { 30 | su $ST2110_USER -c "ebu_list_ctl log" 31 | } 32 | 33 | setup_iface() 34 | { 35 | if [ -z $1 ]; then return; fi 36 | iface=$1 37 | if [ ! -d /sys/class/net/$iface ]; then 38 | log_st2110 "$iface doesn't exist, exit." 39 | fi 40 | 41 | if [ $(cat /sys/class/net/$iface/operstate) != "up" ]; then 42 | log_st2110 "$iface is not up, exit." 43 | fi 44 | 45 | # doesn't look to work with MT27800 Family [ConnectX-5] 46 | #ethtool --set-priv-flags $iface sniffer on 47 | ethtool -G $iface rx 100 # trigger a change so to avoid exiting 48 | ethtool -G $iface rx 8192 49 | 50 | # HW timestamp any incoming pkt 51 | hwstamp_ctl -i $iface -r 1 52 | } 53 | 54 | log_system(){ 55 | journalctl -xef -n 1000 | grep 2110 56 | } 57 | 58 | usage() 59 | { 60 | echo "Usage: $0 {start|stop|log} 61 | log " >&2 62 | } 63 | case "$1" in 64 | start) 65 | setup_iface $MEDIA_IFACE_0 66 | setup_iface $MEDIA_IFACE_1 67 | # TODO: announce hostname through lldpd 68 | smcrouted 69 | ;; 70 | stop) 71 | smcroutectl kill 72 | ;; 73 | conf) # hidden 74 | bash -c "cd /home/$ST2110_USER/st2110-toolkit; source ./install.sh; install_config" 75 | ;; 76 | log) 77 | case "$2" in 78 | list) 79 | log_list 80 | ;; 81 | system) 82 | log_system 83 | ;; 84 | *) 85 | usage 86 | exit 1 87 | ;; 88 | esac 89 | ;; 90 | *) 91 | usage 92 | exit 1 93 | ;; 94 | esac 95 | 96 | exit 0 97 | 98 | -------------------------------------------------------------------------------- /doc/SW_source.md: -------------------------------------------------------------------------------- 1 | # SMPTE 2110 Software source 2 | 3 | If no source available, [gstreamer](https://gstreamer.freedesktop.org/) 4 | can generate adequate streams. The following command: 5 | * generates a video raw signal, 4:2:2 8-bit 6 | * assembles a udp payload of type 102 7 | * streams to multicast @ 239.0.0.0:5005 8 | * generates an audio raw signal, 24-bit linear, 2 channels 9 | * assembles a udp payload of type 103 10 | * streams to multicast @ 239.0.0.0:5007 11 | 12 | ``` 13 | gst-launch-1.0 rtpbin name=rtpbin \ 14 | videotestsrc horizontal-speed=2 ! \ 15 | video/x-raw,width=1920,height=1080,framerate=30/1,format=UYVP ! rtpvrawpay pt=102 ! queue ! \ 16 | rtpbin.send_rtp_sink_0 rtpbin.send_rtp_src_0 ! queue ! \ 17 | udpsink host=239.0.0.0 port=5005 render-delay=0 rtpbin.send_rtcp_src_0 ! \ 18 | udpsink host=239.0.0.0 port=5005 sync=false async=false \ 19 | audiotestsrc ! audioresample ! audioconvert ! \ 20 | rtpL24pay ! application/x-rtp, pt=103, payload=103, clock-rate=48000, channels=2 ! \ 21 | rtpbin.send_rtp_sink_1 rtpbin.send_rtp_src_1 ! \ 22 | udpsink host=239.0.0.0 port=5007 render-delay=0 rtpbin.send_rtcp_src_1 ! \ 23 | udpsink host=239.0.0.0 port=5007 sync=false async=false 24 | ``` 25 | -------------------------------------------------------------------------------- /doc/closed_captions.md: -------------------------------------------------------------------------------- 1 | # Closed captions 2 | 3 | ## Definitions 4 | 5 | [Best intro to closed captions.](https://www.adobe.com/content/dam/acom/en/devnet/video/pdfs/introduction_to_closed_captions.pdf) 6 | 7 | ### Input 8 | 9 | SMPTE ST 2110-40 defines encapsulation scheme looks like: 10 | 11 | ``` 12 | udp > rtp > ancillary data (SMPTE ST 291M) > CC (EIA-608/708) 13 | ``` 14 | 15 | ### Output 16 | 17 | Closed captions can be conveyed as a **distinct stream** in containers: 18 | 19 | * MPEG TS -> DVB teletext (DVB subtitles are bitmaps) (mostly used in EU) 20 | * RTMP -> AMF messages 21 | * MP4, MOV -> text 22 | * MKV -> ass, srt 23 | * HLS -> WebVTT (dedicated file) 24 | 25 | 608/708 captions can also be **embedded in H264 SEI NALU** according to SCTE-128 and ASTC A/53 (North America) 26 | 27 | ## FFmpeg capabilities and limitations 28 | 29 | ### Demuxing 30 | 31 | The demuxing part doesn't exist but a draft was implemented ([*dev/cc/v0* git branch](https://github.com/cbcrc/FFmpeg/commits/dev/cc/v0)) 32 | 33 | ### Decoding 34 | 35 | The CC can easily be extracted to *srt* file, which validates the demuxer and EIA-608/708 decoder. 36 | 37 | ### Remuxing in container 38 | 39 | As *srt*, *text* and *mov_text* CC codec work fine, ffmpeg is able to embed CC in **file-type containers**: MKV, MOV, MP4. WebVTT seems to be supported as well but it wasn't tested. 40 | 41 | For DVB teletext, JEEB (IRC fellow) suggests to use *libzvbi* to write an encoder. So far, this library has been integrated for teletext decoding only (*libavcodec/libzvbi-teletextdec.c*). 42 | 43 | [Devin H.](mailto:dheitmueller@kernellabs.com) from [Kernel Labs](http://www.kernellabs.com) also recommends the teletext approach like *libavdevice/decklink_dec.cpp*, which, btw, also relies on *libzbi*. However, he admits it wasn't tested for TS output. 44 | 45 | For FLV/RTMP, AMF messages should be used to convey CC, more precisely *onCaptionInfo* or *onCuePoint* events. In ffmpeg, the flv decoder can handle such messsages but not the encoder. No relevant example could be found. 46 | 47 | ### Embed H264 in SEI data 48 | 49 | ffmpeg can [preserve CC for H264 pass-through](https://trac.ffmpeg.org/ticket/1778]) but **hardly can merge a data track to a video track**. 50 | 51 | Here are some suggestions collected: 52 | 53 | * JEEB recommends to use libav API, which has more potentials than the ffmpeg tool, to implement a dedicated app. 54 | * Kernel Labs: 55 | > implement a decoder which takes the AVPackets containing 56 | > CC data and creates AVFrames, and then create what ffmpeg refers to as 57 | > a "multimedia" filter which takes in the video and data packets and 58 | > outputs video packets that contain the side data. Multimedia filters 59 | > are what are used to do things like taking in audio and video AVFrames 60 | > from two streams and burning audio bars into the resulting video. 61 | > Architecturally this is probably the "right" approach, but would 62 | > require some changes to the frameworks and ffmpeg.c because today 63 | > libavfilter currently only supports audio and video AVFrames 64 | 65 | ## Misc 66 | 67 | Tested command line: 68 | 69 | ```sh 70 | ./ffmpeg -y -loglevel info -strict experimental -threads 2 -buffer_size 671088640 -protocol_whitelist file,udp,rtp -i /home/transcoder/sdp/emb_176_explora_anc.sdp -fifo_size 1000000000 -smpte2110_timestamp 1 -passlogfile /tmp/ffmpeg2pass -c:a libfdk_aac -ac 2 -b:a 128k -r 30 -vf yadif=0:-1:0 -s 1280x720 -pix_fmt yuv420p -c:v libx264 -profile:v main -preset fast -level:v 3.1 -b:v 2500k -bufsize:v 5000k -maxrate:v 2500k -a53cc 1 -x264-params b-pyramid=1 -g 30 -keyint_min 16 -pass 1 -refs 6 -scodec text -f tee -map 0:v -map 0:s -map 0:a "[f=mpegts]/tmp/toto.ts|[f=mpegts]udp://@10.177.45.127:5000|[select=\'s:0\']/tmp/toto.srt" 71 | ``` 72 | 73 | where *-scodec* option can take the following values. 74 | 75 | ```sh 76 | ffmpeg -encoders | grep "^ S" 77 | [...] 78 | S..... = Subtitle 79 | S..... ssa ASS (Advanced SubStation Alpha) subtitle (codec ass) 80 | S..... ass ASS (Advanced SubStation Alpha) subtitle 81 | S..... dvbsub DVB subtitles (codec dvb_subtitle) 82 | S..... dvdsub DVD subtitles (codec dvd_subtitle) 83 | S..... mov_text 3GPP Timed Text subtitle 84 | S..... srt SubRip subtitle (codec subrip) 85 | S..... subrip SubRip subtitle 86 | S..... text Raw text subtitle 87 | S..... webvtt WebVTT subtitle 88 | S..... xsub DivX subtitles (XSUB) 89 | ``` 90 | 91 | [AWS MediaLive](https://docs.aws.amazon.com/medialive/latest/ug/general-information-supported-formats.html) supports a couple of captions formats. 92 | 93 | [Kernel Labs](http://www.kernellabs.com) developped [libklanc](https://github.com/stoth68000/libklvanc) as a codec for multitple types of ancillary. The decoding part is used in *libavdevice/decklink_dec.cpp*. 94 | 95 | [This MXF use case](https://trac.ffmpeg.org/ticket/5362)(*libavformat/mxfdec.c*) is another example where CC is extracted but can't be injected in h264 or TS. 96 | -------------------------------------------------------------------------------- /doc/embrionix.md: -------------------------------------------------------------------------------- 1 | ## Embrionix encapsulator: 2 | 3 | * has 2 SDI inputs A and B 4 | * encapsulates 1 video, 8 audio and 1 ancillary flow for each input 5 | * ouputs 2 (1 and 2) RTP streams per flow for -7 6 | * provides normal, per-flow SDPs 7 | * provides -7 SDPs, with primary and secondary flow combined (used by NMOS API) 8 | * has a unicast IP address for control 9 | * has a fake source IP address for media, it is not pingable and is used 10 | for src IP only, in packets 11 | 12 | The 40 flows are ordered this way: 13 | 14 | * 0: video A1 15 | * 1: video A2 16 | * 2: audio ch1 A1 17 | * 3: audio ch1 A2 18 | * [...] 19 | * 16: audio ch8 A1 20 | * 17: audio ch8 A2 21 | * 18: ancillary A1 22 | * 19: ancillary A2 23 | * 20: video B1 24 | * 21: video B2 25 | * [...] 26 | * 38: ancillary B1 27 | * 39: ancillary B2 28 | 29 | get_sdp.py uses Embrionix API to fetch SDP per flow. 30 | 31 | # API 32 | 33 | Firmware info: http:// 34 | 35 | API entry point: http:///emsfp/node/v1 36 | 37 | # ancillary: 38 | 39 | RTP time step: 1501 or 1502 ticks (interlaced) 40 | 41 | Version 3.1.1673 42 | 43 | 3 modes: 44 | 45 | * 'End of field event': 1pkt/field, compliant 46 | * '1 ms of decoding': 2pkt/field, wrong marker bit, not compliant 47 | * 'Packet by Packet': 1 anc type / pkt + 1 empty pkt for marker 48 | 49 | # Frame sync 50 | 51 | * no frame_sync is not frame_sync=0us 52 | * rollover: frame_sync = N % 1448 53 | -------------------------------------------------------------------------------- /doc/hw_encoding.md: -------------------------------------------------------------------------------- 1 | ## Hardware acceleration for transcoding 2 | 3 | Proposed setup for hardware-accelerated scaling and encoding: 4 | 5 | * GPU Model: Nvidia Quadro P4000 6 | * GPU arch: Pascal GP104 7 | * Centos: 7 8 | * Kernel + header: 3.10 9 | * Gcc: 4.8.5 10 | * Glibc: 2.17 11 | * CUDA Driver 10.1 12 | * CUDA Runtime 10.0 13 | * Nvidia driver: 418.43 14 | 15 | ### Nvidia driver 16 | 17 | * [Linux driver installation guide.](https://linuxconfig.org/how-to-install-the-nvidia-drivers-on-centos-7-linux) 18 | * [Download v415.18](https://www.nvidia.com/Download/driverResults.aspx/142958/en-us) 19 | 20 | ```sh 21 | $ chmod 755 NVIDIA-Linux-x86_64-418.43.run 22 | $ ./NVIDIA-Linux-x86_64-418.43.run -h 23 | $ ./NVIDIA-Linux-x86_64-418.43.run -x 24 | $ cd ./NVIDIA-Linux-x86_64-418.43 25 | $ ./nvidia-installer # can --uninstall 26 | ``` 27 | 28 | Verify the driver is loaded: 29 | 30 | ```sh 31 | $ lsmod | grep nvidia 32 | $ cat /proc/driver/nvidia/version 33 | $ ./nvidia-smi 34 | ``` 35 | 36 | [Complete install doc](http://http.download.nvidia.com/XFree86/Linux-x86_64/418.43/README/) 37 | 38 | ### CUDA SDK 39 | 40 | * [Installation guide](https://developer.download.nvidia.com/compute/cuda/10.0/Prod/docs/sidebar/CUDA_Installation_Guide_Linux.pdf) 41 | * [Download v10.0](https://developer.nvidia.com/cuda-downloads?target_os=Linux&target_arch=x86_64&target_distro=CentOS&target_version=7&target_type=rpmnetwork) 42 | 43 | Verify that CUDA can talk to GPU card: 44 | 45 | ```sh 46 | ~/cuda-10.0-samples/NVIDIA_CUDA-10.0_Samples/1_Utilities/deviceQuery/deviceQuery 47 | [...] 48 | Device 0: "Quadro P4000" 49 | CUDA Driver Version / Runtime Version 10.1 / 10.0 50 | [...] 51 | ``` 52 | 53 | ### Nvidia codec for ffmpeg: 54 | 55 | `NVENC` needs for custom headers maintained outside of `ffmpeg` sources. 56 | 57 | [ffmpeg doc for NVENC](https://trac.ffmpeg.org/wiki/HWAccelIntro#NVENC) 58 | 59 | This is added in the install script. 60 | 61 | ## Measuring CPU and GPU utilization 62 | 63 | ```sh 64 | $ vmstat -w -n 1 # check "us" (user) column 65 | $ nvidia-smi dmon -i 0 # check "enc" column 66 | ``` 67 | 68 | ### Troubleshoot 69 | 70 | Got this message after ffmpeg version bumped: 71 | 72 | ``` 73 | [h264_nvenc @ 0x25d3440] Driver does not support the required nvenc API version. Required: 9.0 Found: 8.1 74 | [h264_nvenc @ 0x25d3440] The minimum required Nvidia driver for nvenc is 390.25 or newer 75 | ``` 76 | 77 | Version doesn't seem to match anything but bumping the driver from 415 to 418 solved it. 78 | -------------------------------------------------------------------------------- /doc/nmos.md: -------------------------------------------------------------------------------- 1 | # AMWA NMOS 2 | 3 | [NMOS](https://amwa-tv.github.io/nmos/) addresses the management of ST2110-based infrastructure regarding: 4 | 5 | * device discovery/registration 6 | * device self description of capabilities 7 | * connections between senders and receiver 8 | 9 | In order to experiment with this standard, the proposed resources rely 10 | on docker containerization which allows to easily deploy virtual NMOS 11 | nodes and registry. 12 | 13 | ## Setup the Docker image 14 | 15 | You can either build the node Docker image from top folder: 16 | 17 | ```sh 18 | docker build -t nmos-cpp:v0 -f nmos/Dockerfile . 19 | ``` 20 | 21 | Or you can fetch from Docker registry: 22 | 23 | ```sh 24 | docker build pk1984/nmos-cpp:v0 25 | ``` 26 | 27 | ## Execution 28 | 29 | Start, for instance, a registry from a config stored on the host: 30 | 31 | ```sh 32 | id=$(docker run -d -v /local/path/to/nmos/reg.conf:/tmp/reg.conf -ti nmos-cpp:v0 nmos-cpp-registry /tmp/reg.conf) 33 | ``` 34 | 35 | And monitor: 36 | 37 | ```sh 38 | docker attach $id 39 | # Ctrl+p, Ctrl-q to exit the container 40 | ``` 41 | -------------------------------------------------------------------------------- /doc/scte_104_to_35.md: -------------------------------------------------------------------------------- 1 | # SCTE-104 to SCTE-35 2 | 3 | ## Definitions 4 | 5 | * SCTE-104 -> SDI and SMPTE ST 2110-40 (did=0x41, sdid=0x07) 6 | * SCTE-35 -> MPEG TS 7 | 8 | ## References 9 | 10 | [Kernel Labs](http://www.kernellabs.com) has documented their [SDI-to-TS transcoder](http://www.kernellabs.com/blog/?p=4251) and has intended to [upstream in ffmpeg](https://patchwork.ffmpeg.org/patch/7221/) the integration of their [library](https://github.com/stoth68000/libklscte35). Here is their recommendation: 11 | > No new work would be needed to the libklvanc nor libklscte35 12 | > libraries, as we have this use case working in a publicly available 13 | > build of the Open Broadcast Encoder. In terms of ffmpeg integration, 14 | > I was primarily focused on the opposite use case - extracting SCTE-35 15 | > from a transport stream, converting it to SCTE-104, and putting it out 16 | > over SDI as VANC. While I certainly intend to support the use case 17 | > you're describing and indeed I had it working in the lab at one point, 18 | > the functionality isn't stable and needs more time/energy to get it 19 | > into production. In particular, an ffmpeg bitstream filter needs to 20 | > be written to do the conversion (we have a "scte35 to scte104" filter, 21 | > but you need a filter which does the opposite), and the mpeg TS muxing 22 | > module needs to be modified to embed the resulting SCTE-35 packets. 23 | > It isn't a huge piece of work, but it's not something I'm in a 24 | > position to release yet as I can't claim it's fully stable 25 | -------------------------------------------------------------------------------- /doc/sdp.sample: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 1443716955 1443716955 IN IP4 172.30.64.176 3 | s=st2110 stream 4 | t=0 0 5 | 6 | m=audio 20000 RTP/AVP 97 7 | c=IN IP4 225.0.1.16/64 8 | a=source-filter: incl IN IP4 225.0.1.16 172.30.64.176 9 | a=rtpmap:97 L24/48000/8 10 | a=mediaclk:direct=0 rate=48000 11 | a=framecount:48 12 | a=ptime:1 13 | a=ts-refclk:ptp=IEEE1588-2008:00-02-c5-ff-fe-21-60-5c:127 14 | 15 | m=video 20000 RTP/AVP 100 16 | c=IN IP4 225.17.0.16/64 17 | a=source-filter: incl IN IP4 225.17.0.16 172.30.64.176 18 | a=rtpmap:100 smpte291/90000 19 | a=fmtp:100 VPID_Code=133 20 | a=mediaclk:direct=0 rate=90000 21 | a=ts-refclk:ptp=IEEE1588-2008:00-02-c5-ff-fe-21-60-5c:127 22 | 23 | m=video 20000 RTP/AVP 96 24 | c=IN IP4 225.164.5.190/64 25 | a=source-filter: incl IN IP4 225.164.5.190 172.30.64.176 26 | a=rtpmap:96 raw/90000 27 | a=fmtp:96 sampling=YCbCr-4:2:2; width=1920; height=1080; exactframerate=30000/1001; depth=10; TCS=SDR; colorimetry=BT709; PM=2110GPM; SSN=ST2110-20:2017; TP=2110TPN; interlace; 28 | a=mediaclk:direct=0 29 | a=ts-refclk:ptp=IEEE1588-2008:00-02-c5-ff-fe-21-60-5c:127 30 | -------------------------------------------------------------------------------- /doc/transcoder_perf.md: -------------------------------------------------------------------------------- 1 | # Transcoding performance 2 | 3 | ## Setup 4 | 5 | * ffmpeg v4.2 + ST 2110 patch (see 5.1 at the bottom) 6 | * Centos 7 virtualized 7 | * 4 x Intel(R) Xeon(R) Gold 6142 CPU @ 2.60GHz 8 | * memory 4GB 9 | * GPU Model: Nvidia Quadro P4000, PassThrough 10 | * network adapter: VMXNET 3, DirectPath I/O 11 | 12 | ## Load 13 | 14 | * input: 1 or 2 streams @ 1080i 60 fps (1.23Gb/s) + 2 audio channels 15 | * output: 720p @ 30 fps (2.5Mpbs) + 2 audio channels 16 | * audio-only test is performed at the end. 17 | 18 | ## Measuring CPU and GPU utilization 19 | 20 | ```sh 21 | $ vmstat -n 1 # check "us" (user) column 22 | $ nvidia-smi dmon -i 0 # check "enc" column 23 | ``` 24 | 25 | ## CPU encoding (libx264) 26 | 27 | ### 1 stream 28 | 29 | ``` 30 | # ./transcode start -e cpu ../sdp/emb_176_explora.sdp 31 | /usr/local/bin/ffmpeg -loglevel info -strict experimental -threads 2 -buffer_size 671088640 -protocol_whitelist file,udp,rtp -i ../sdp/emb_176_explora.sdp -fifo_size 1000000000 -smpte2110_timestamp 1 -r 30 -vf yadif=0:-1:0 -s 1280x720 -pix_fmt yuv420p -c:v libx264 -profile:v main -preset fast -level:v 3.1 -b:v 2500k -bufsize:v 7000k -maxrate:v 2500k -x264-params b-pyramid=1 -g 30 -keyint_min 16 -pass 1 -refs 6 -c:a libfdk_aac -ac 2 -b:a 128k -f tee -map 0:v -map 0:a "[f=mpegts]udp://10.177.45.127:5001?pkt_size=1316" 32 | ``` 33 | 34 | * CPU: 44% (user=30%, sys=8%), Mem: 16% 35 | 36 | ### 2 streams 37 | 38 | * CPU: 97% (user=60%, sys=6%), Mem: 16% 39 | * packet drops after a few sec. 40 | * stable with 8 CPUs instead of 4 41 | 42 | ## GPU encoding (h264_nvenc) 43 | 44 | ### 1 stream 45 | 46 | ``` 47 | # ./transcode start -e gpu ../sdp/emb_176_explora.sdp 48 | /usr/local/bin/ffmpeg -loglevel info -strict experimental -threads 2 -buffer_size 671088640 -protocol_whitelist file,udp,rtp -i ../sdp/emb_176_explora.sdp -fifo_size 1000000000 -smpte2110_timestamp 1 -r 30 -vf yadif=0:-1:0,format=yuv420p,hwupload_cuda,scale_npp=w=1280:h=720:format=yuv420p:interp_algo=lanczos,hwdownload,format=yuv420p -c:v h264_nvenc -rc cbr_hq -preset:v fast -profile:v main -level:v 4.1 -b:v 2500k -bufsize:v 7000k -maxrate:v 2500k -g 30 -keyint_min 16 -pass 1 -refs 6 -c:a libfdk_aac -ac 2 -b:a 128k -f tee -map 0:v -map 0:a "[f=mpegts]udp://10.177.45.127:5001?pkt_size=1316" 49 | ``` 50 | 51 | CPU: 33% (user: 23%, sys: 8%), Mem: 16% 52 | 53 | GPU: 4%, 211MiB / 8119MiB 54 | 55 | ### 2 streams 56 | 57 | * CPU: 76% (user=52%, sys=20%), Mem: 30% 58 | * GPU: 8%, 413MiB / 8119MiB 59 | * jerky, slow, and packet drop after a few minutes 60 | * stable with 6 CPUs instead of 4 61 | 62 | ## GPU encoding 1 source @ mutlitple resolutions/bitrates 63 | 64 | Since CPU encoding is quite limited let's focuse on GPU for multiple 65 | output. 66 | 67 | Multiple FFmpeg instances is not adequate because they all read from the 68 | same socket, i.e. there is only one multicast IGMP join, and they all 69 | fight for the data. Use one FFmpeg instance to transcode one feed into 70 | multiple resolution/bitrate output (3500k, 2500k, 1200k, 800k, 500, 71 | 400k, 250k) is prefered. 72 | 73 | ```sh 74 | # ./transcode start -e gpu -o multi ../sdp/emb_176_explora.sdp 75 | /usr/local/bin/ffmpeg -loglevel info -strict experimental -threads 2 -buffer_size 671088640 -protocol_whitelist file,udp,rtp -i ../sdp/emb_176_explora.sdp 76 | -fifo_size 1000000000 -smpte2110_timestamp 1 -c:a libfdk_aac -ac 2 -b:a 128k -r 30 77 | -filter_complex '[0:v]split=4[in0][in1][in2][in3];\ 78 | [in0]format=yuv420p,hwupload_cuda,scale_npp=w=1280:h=720:format=yuv420p:interp_algo=lanczos,hwdownload,format=yuv420p,split=2[out0][out1];\ 79 | [in1]format=yuv420p,hwupload_cuda,scale_npp=w=852:h=480:format=yuv420p:interp_algo=lanczos,hwdownload,format=yuv420p[out2];\ 80 | [in2]format=yuv420p,hwupload_cuda,scale_npp=w=640:h=360:format=yuv420p:interp_algo=lanczos,hwdownload,format=yuv420p,split=2[out3][out4];\ 81 | [in3]format=yuv420p,hwupload_cuda,scale_npp=w=480:h=270:format=yuv420p:interp_algo=lanczos,hwdownload,format=yuv420p,split=2[out5][out6]'\ 82 | -map [out0] -c:v h264_nvenc -rc cbr_hq -preset:v fast -profile:v main -level:v 4.1 -b:v 3500k -bufsize:v 7000k -maxrate:v 3500k -g 30 -keyint_min 16 -pass 1 -refs 6 -f mpegts udp://10.177.45.127:5000?pkt_size=1316\ 83 | -map [out1] -c:v h264_nvenc -rc cbr_hq -preset:v fast -profile:v main -level:v 4.1 -b:v 2500k -bufsize:v 5000k -maxrate:v 2500k -g 30 -keyint_min 16 -pass 1 -refs 6 -f mpegts udp://10.177.45.127:5001?pkt_size=1316\ 84 | -map [out2] -c:v h264_nvenc -rc cbr_hq -preset:v fast -profile:v main -level:v 4.1 -b:v 1200k -bufsize:v 2500k -maxrate:v 1200k -g 30 -keyint_min 16 -pass 1 -refs 6 -f mpegts udp://10.177.45.127:5002?pkt_size=1316\ 85 | -map [out3] -c:v h264_nvenc -rc cbr_hq -preset:v fast -profile:v baseline -level:v 4.1 -b:v 800k -bufsize:v 1600k -maxrate:v 800k -g 30 -keyint_min 16 -pass 1 -refs 6 -f mpegts udp://10.177.45.127:5003?pkt_size=1316 -map [out4] -c:v h264_nvenc -rc cbr_hq -preset:v fast -profile:v baseline -level:v 4.1 -b:v 500k -bufsize:v 1000k -maxrate:v 500k -g 30 -keyint_min 16 -pass 1 -refs 6 -f mpegts udp://10.177.45.127:5004?pkt_size=1316\ 86 | -map [out5] -c:v h264_nvenc -rc cbr_hq -preset:v fast -profile:v baseline -level:v 3.1 -b:v 400k -bufsize:v 800k -maxrate:v 400k -g 30 -keyint_min 16 -pass 1 -refs 6 -f mpegts udp://10.177.45.127:5005?pkt_size=1316\ 87 | -map [out6] -c:v h264_nvenc -rc cbr_hq -preset:v fast -profile:v baseline -level:v 3.1 -b:v 250k -bufsize:v 800k -maxrate:v 250k -g 30 -keyint_min 16 -pass 1 -refs 6 -f mpegts udp://10.177.45.127:5006?pkt_size=1316 88 | ``` 89 | 90 | * GPU 42% (user: 26%, 11%), Mem: 55% 91 | * GPU: 10%, 1030MiB / 8119MiB 92 | * some packet drops 93 | * stable with 6 CPUs instead of 4 94 | * command line is too long to run in tmux 95 | * no yadif and no audio because it made the filter graph very complicated 96 | 97 | ## Audio only, 98 | 99 | The load is composed of 8 audio stream (L24/48k/2). The VM settings and 100 | the audio encoding parameters are the same as above. 101 | 102 | * Input rate: 21Mb/s 103 | * Output rate: 1.3Mb/s 104 | 105 | CPU: 5-25%, Mem: 9% 106 | 107 | ## Update with ffmpeg v5.1 108 | 109 | ### Setup 110 | 111 | * ffmpeg v5.1 + input thread patch 112 | * input: 1 stream @ 1080i 60 fps (1.23Gb/s) + 2 audio channels 113 | * output: 720p @ 30 fps (2.5Mpbs) + 2 audio channels 114 | 115 | ### CPUs comparison 116 | 117 | | CPU | usage | Comment | 118 | | --- | ---- | ------- | 119 | | 4 x Intel(R) Xeon(R) Gold 6142 CPU @ 2.60GHz | | stable | 120 | | 4 x Intel(R) Xeon(R) Silver 4114 CPU @ 2.20GHz | | packet drops | 121 | | 6 x Intel(R) Core(TM) i5-9600K CPU @ 3.70GHz | | stable | 122 | -------------------------------------------------------------------------------- /doc/troubleshoot.md: -------------------------------------------------------------------------------- 1 | # Troubleshoot 2 | 3 | When a new system is up, you should validate a few steps before 4 | using a media application like EBU-LIST or ffmpeg: 5 | 6 | * IGMP works 7 | * the NIC sees the stream 8 | * the application sees the stream 9 | 10 | ## IGMP: is it possible to join a multicast stream? 11 | 12 | You can validate that the multicast IGMP group is joined and that data 13 | is received thanks to the socket reader: 14 | 15 | ```sh 16 | $ cd capture/ 17 | $ gcc -o socket_reader -std=c99 socket_reader.c 18 | $ ./socket_reader -g -p -i 19 | ``` 20 | 21 | Validate that the multicast group is joined through the correct 22 | interface: 23 | 24 | ```sh 25 | netstat -ng | grep 26 | ``` 27 | 28 | 29 | When capturing traffic (as opposed to transcoding), if `smcroute` returns this error, restart the daemon: 30 | 31 | ``` 32 | Daemon error: Join multicast group, unknown interface eth0 33 | $ sudo /etc/init.d/smcroute restart 34 | ``` 35 | 36 | Measure the UDP packet drops: 37 | 38 | ```sh 39 | netstat -s -u 40 | ``` 41 | 42 | ## NIC: is the stream present? 43 | 44 | `tcpdump` is our friend but it can't guess on which interface to throw the IGMP join request. 45 | You need to create a static route before: 46 | 47 | ```sh 48 | ip route add via dev 49 | tcpdump -i 50 | ``` 51 | 52 | Verify that multicast is joined using the correct interface with `netstat -ng`. 53 | 54 | ## App: is the stream visible? 55 | 56 | Re-use the `socket_reader`: 57 | 58 | ```sh 59 | $ ./socket_reader -g -p -i 60 | Detected stream with payload type 96 61 | Missed or missing RTP marker 62 | ^Creceived SIGINT 63 | received: 857470 64 | dropped: 14564 65 | 1.67 % drop 66 | ``` 67 | 68 | If the stream can be seen by `tcpdump` but not by an app like 69 | `socket_reader`, it can either be blocked by the firewall or the stream 70 | source verification. Let's take the example of Centos for which security 71 | is tighter than Debian. 72 | 73 | First the firewall must let the UDP port in: 74 | 75 | ```sh 76 | firewall-cmd --zone=public --add-port=20000/udp --permanent 77 | firewall-cmd --reload 78 | ``` 79 | 80 | Then, the source of the stream must be verified by either create a 81 | static route: 82 | 83 | ```sh 84 | ip route add via dev 85 | ``` 86 | 87 | OR disable the reverse path filter: 88 | 89 | ```sh 90 | sysctl -w net.ipv4.conf.all.rp_filter=0 91 | sysctl -w net.ipv4.conf..rp_filter=0 92 | ``` 93 | 94 | Create a new file in `/usr/lib/sysctl.d/` for persistency. 95 | 96 | -------------------------------------------------------------------------------- /ebu-list/README.md: -------------------------------------------------------------------------------- 1 | # EBU-LIST server integration guide 2 | 3 | 4 | 5 | - [Overview](#overview) 6 | - [Suggested Hardware + OS](#suggested-hardware--os) 7 | * [Part list](#part-list) 8 | * [OS](#os) 9 | + [Boot Ubuntu 20.04 from USB stick.](#boot-ubuntu-2004-from-usb-stick) 10 | + [OS install](#os-install) 11 | + [OS init setup](#os-init-setup) 12 | * [RAID 0 array for user data](#raid-0-array-for-user-data) 13 | - [Install ST 2110 dependencies](#install-st-2110-dependencies) 14 | * [PTP](#ptp) 15 | * [NMOS](#nmos) 16 | * [Capture Engine (DPDK + Nvidia/Mellanox ConnectX-5](#capture-engine-dpdk--nvidiamellanox-connectx-5) 17 | - [EBU-LIST](#ebu-list) 18 | * [Setup](#setup) 19 | * [Controls](#controls) 20 | * [Upgrade](#upgrade) 21 | - [Storage](#storage) 22 | - [TODO:](#todo) 23 | 24 | 25 | 26 | ## Overview 27 | 28 | This is the integration guide for [EBU LIST](https://tech.ebu.ch/list). 29 | Although the project documentation allows to setup an offline analyzer, 30 | this guide gives instructions to build a standalone, high performance 31 | capturing devices. 32 | 33 | * [Online running instance: EBU LIST](http://list.ebu.io/login) (no capture) 34 | * [Sources](https://github.com/ebu/pi-list) 35 | 36 | Sponsored by: 37 | 38 | ![logo](https://site-cbc.radio-canada.ca/site/annual-reports/2014-2015/_images/about/services/cbc-radio-canada.png) 39 | 40 | ## Suggested Hardware + OS 41 | 42 | ### Part list 43 | 44 | | What | Item | Qty | 45 | |------|------|-----| 46 | |Motherboard|[Gigabyte Z390 AORUS PRO Wifi Intel Z390/socket1151 rev 1.0](https://www.gigabyte.com/ca/Motherboard/Z390-I-AORUS-PRO-WIFI-rev-10)| 1 | 47 | |CPU|[Intel Core i5-9600K Coffee Lake 6-Core 3.7 GHz (4.6 GHz Turbo) LGA 1151 (300 Series) 95W BX80684I59600K Desktop Processor Intel UHD Graphics 630](https://www.newegg.ca/core-i5-9th-gen-intel-core-i5-9600k/p/N82E16819117959)| 1 | 48 | |RAM|[G.SKILL Aegis 16GB (2 x 8GB) 288-Pin DDR4 SDRAM DDR4 3000 (PC4 24000) Intel Z170 Platform Memory (Desktop Memory) Model F4-3000C16D-16GISB ](https://www.newegg.ca/g-skill-16gb-288-pin-ddr4-sdram/p/N82E16820232417)| 1 | 49 | |SSD for user data|[SAMSUNG 860 EVO Series 2.5" 500GB SATA III V-NAND 3-bit MLC Internal Solid State Drive (SSD) MZ-76E500B/AM](https://www.newegg.ca/samsung-860-evo-series-500gb/p/N82E16820147674) | 2 | 50 | |SATA III cable|[Coboc Model SC-SATA3-18 18" SATA III 6Gb/s Data Cable](https://www.newegg.ca/p/N82E16812422752?Description=SATA%20III%20&cm_re=SATA_III-_-12-422-752-_-Product)| 2 | 51 | |NVMe SSD for OS|[Samsung PM981 Polaris 256GB M.2 NGFF PCIe Gen3 x4, NVME SSD, OEM (2280) MZVLB256HAHQ-00000](https://www.newegg.ca/samsung-pm981-256gb/p/0D9-0009-002R4)| 1 | 52 | |NVMe for data cache|[Intel Optane M.2 2280 32GB PCIe NVMe 3.0 x2 Memory Module/System Accelerator MEMPEK1W032GAXT](https://www.newegg.ca/intel-optane-32gb/p/N82E16820167427)| 1 | 53 | |Network controller|[Mellanox Connectx-5](https://www.newegg.ca/p/14U-005H-00068)| 1 | 54 | |Thermal compound|[Arctic Silver AS5-3.5G Thermal Compound](https://www.newegg.ca/arctic-silver-as5-3-5g/p/N82E16835100007)| 1 | 55 | |Heat sink|[Noctua NH-L9i, Premium Low-profile CPU Cooler for Intel LGA115x](https://www.newegg.ca/p/N82E16835608029)| 1 | 56 | |Computer case|[APEVIA X-FIT-200 Black Steel Mini-ITX Tower Computer Case 250W Power Supply](https://www.newegg.ca/p/N82E16811144255)| 1 | 57 | |Replacement Power Supply|[Sylver Stone FX350-G](https://www.silverstonetek.com/product.php?pid=784&area=en)| 1 | 58 | 59 | TODO: photos 60 | 61 | ### OS 62 | 63 | #### Boot Ubuntu 20.04 from USB stick. 64 | 65 | * [Create a bootable USB stick with Ubuntu 20.04 inside](https://tutorials.ubuntu.com/tutorial/tutorial-create-a-usb-stick-on-ubuntu#0) 66 | * plug the USB on the station and power up 67 | * press F2 to enter the BIOS setup. 68 | * select UEFI USB stick as a primary boot device 69 | * set correct time 70 | * set `AC back` = `Always on` (auto startup on power failure) 71 | * `Fan failure warning` = `on` 72 | * save and exit BIOS 73 | 74 | #### OS install 75 | 76 | * start Ubuntu installer 77 | * select "Minimal installation" 78 | * no disk encryption nor LVM required 79 | * select target disk for OS, i.e. the largest NVMe 80 | * user: ebulist 81 | * restart 82 | 83 | #### OS init setup 84 | 85 | From here, use the terminal. Install basic tools: 86 | 87 | ```sh 88 | sudo -i 89 | apt udpate 90 | apt udgrade 91 | apt install openssh-server git 92 | ``` 93 | 94 | OS update may break Mellanox drivers, see `install_mellanox` function 95 | for detail. Disable automatic update in `/etc/apt/apt.conf.d/20auto-upgrades` (need for root priviledges). 96 | 97 | ```sh 98 | APT::Periodic::Update-Package-Lists "0"; 99 | ``` 100 | 101 | ### RAID 0 array for user data 102 | 103 | Raid 0 consists in splitting data into segment and writting portions on 104 | multiple disks simultaneous to maximize the throughput. 105 | See [additional performance tests](#throughput) for alternatives. 106 | 107 | Find the 2 SATA drives and create RAID 0 array. From here, most of 108 | installation commands require root priviledges. 109 | 110 | ```sh 111 | sudo -i 112 | apt install mdadm 113 | lsblk | grep sd 114 | mdadm --create --verbose /dev/md0 --level=0 --raid-devices=2 /dev/sda /dev/sdb 115 | ls /dev/md* 116 | cat /proc/mdstat 117 | ``` 118 | 119 | Create an EXT4 file system, create the mount point, mount and set 120 | ownership: 121 | 122 | ```sh 123 | mkfs.ext4 -F /dev/md0 124 | mkdir -p /media/raid0 125 | mount /dev/md0 /media/raid0 126 | chown -R ebulist:ebulist /media/raid0/* 127 | ``` 128 | 129 | For persistent naming: 130 | 131 | ```sh 132 | mdadm --detail --scan >> /etc/mdadm/mdadm.conf 133 | update-initramfs -u # sync ramdisk version of conf file 134 | ``` 135 | 136 | And persitent mounting, add this in `/etc/fstab`: 137 | 138 | ``` 139 | /dev/md0 /media/raid0 ext4 defaults 0 1 140 | ``` 141 | 142 | ## Install ST 2110 dependencies 143 | 144 | As `ebulist` user: 145 | 146 | ```sh 147 | cd ~ 148 | git clone https://github.com/pkeroulas/st2110-toolkit.git 149 | ``` 150 | 151 | As `root` user: 152 | 153 | ```sh 154 | sudo -i 155 | cd /home/ebulist/st2110-toolkit 156 | ./install.sh common 157 | ``` 158 | 159 | Edit master config (`/etc/st2110.conf`), especially the 'Mandatory' part 160 | which contains physical port names, path, etc. This config is loaded by 161 | every script of this toolkit, including EBU-LIST startup script and it 162 | is loaded on ssh login as well. 163 | 164 | ``` 165 | vi /etc/st2110.conf 166 | ``` 167 | 168 | ### PTP 169 | 170 | [Setup instructions.](../ptp/README.md) 171 | 172 | ### NMOS 173 | 174 | [Setup instructions.](../nmos/README.md) 175 | 176 | 177 | ### Capture Engine (DPDK + Nvidia/Mellanox ConnectX-5 178 | 179 | These [instructions](https://github.com/pkeroulas/st2110-toolkit/blob/master/capture/README.md) 180 | show how to setup a performant stream capture engine based on Nvidia/Mellanox NIC + DPDK. 181 | 182 | To integrate dpdk-based capture engine with EBU-LIST, you need to 183 | install the capture agent (nodejs) from EBU-LIST repo itself. 184 | 185 | ```sh 186 | apt install node npm 187 | git clone https://github.com/ebu/pi-list.git 188 | cd app/capture_probe/ 189 | npm install 190 | cd js_common_server 191 | npm install 192 | cd list/ 193 | sudo node server.js config.yml 194 | ``` 195 | 196 | ## EBU-LIST 197 | 198 | ### Setup 199 | 200 | Check the `LIST_*` vars in master config, especially `LIST_DEV` which 201 | determines if EBU-LIST run from sources (`true`) or from docker image 202 | (`false`). 203 | 204 | ```sh 205 | ./install.sh ebulist 206 | ``` 207 | 208 | ### Boot sequence: 209 | 210 | 1. Network, FileSystem and other system services up 211 | 2. st2110.service 212 | - `scmroute` daemon for igmp requests 213 | - media interfaces setup 214 | - hugepages for media application 215 | 3. ptp.service 216 | - ptp4l and phc2sys 217 | 4. nmos.service 218 | - nmos-cpp node 219 | 5. docker.service 220 | 6. ebulist.service 221 | - Docker service: rabbitmq, mongodb, 222 | - Prod: list-server (+nginx) container 223 | - Dev: list-server nodejs (this one always fails at connectiong to mongo at 1st attempts, whereas 224 | other mongosh can. ebu_list_ctl had to be tweaked to detect the error and let systemd restart 225 | the service) 226 | 7. ebulist-probe.service 227 | - home-cooked node app that connect to amq and start dpdk sniffer 228 | 229 | ### Controls 230 | 231 | Control the services with systemctl: 232 | 233 | ```sh 234 | sudo systemctl status|start|stop st2110|ptp|nmos|ebulist|ebulist-probe 235 | ``` 236 | 237 | EBU-LIST itself is controlled by a dedicated script: 238 | 239 | ```sh 240 | $ ebu_list_ctl 241 | /usr/sbin/ebu_list_ctl is a wrapper script that manages EBU-LIST 242 | and related sub-services (DB, backend, UI, etc.) 243 | Usage: 244 | /usr/sbin/ebu_list_ctl [-v][-f] {start|stop|status|log|sniff|freerun_Start|freerun_stop|nmos} 245 | start start docker containers and server 246 | stop stop docker containers and server 247 | status check the status of all the st 2110 services 248 | log get the logs of the server+containers 249 | sniff list incomming udp traffic 250 | freerun_start start a continuous analysis based on ./ebu-list/freerun.sh 251 | freerun_stop stop continuous analysis 252 | nmos probe local nmos RX node 253 | 254 | $ ebu_list_ctl status 255 | ----------------------------------------------- 256 | EBU-LIST Status 257 | ----------------------------------------------- 258 | Hostname XXXXXXXXXXXXXXX 259 | Mgt interface UP eno1 XXXXXXXXXXX 260 | ----------------------------------------------- 261 | Media interfaces 262 | Interface 0 UP XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 263 | Gateway 0 UP XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 264 | Switch 0 UP XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 265 | Interface 1 UP XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 266 | Gateway 1 UP XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 267 | Switch 1 UP XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 268 | ----------------------------------------------- 269 | PTP 270 | ptp4l UP ens3f1 271 | phc2sys 0 UP CLOCK_REALTIME 272 | phc2sys 1 UP ens3f0 273 | Lock UP 274 | PTP traffic UP 275 | ----------------------------------------------- 276 | NMOS 277 | Daemon UP 278 | Config UP 279 | Node API UP 1145b427-a793-513d-b430-0 280 | Connection API UP 281 | Receivers UP 282 | ----------------------------------------------- 283 | Docker 284 | Daemon UP 285 | Network UP 286 | Service Mongo DB UP 287 | Service Influx DB UP 288 | Service Rabbit MQ UP 289 | ----------------------------------------------- 290 | LIST 291 | Profile dev 292 | API running UP 293 | API version UP 2.2.2."36b39758" 294 | GUI running UP 295 | GUI response UP 296 | Pre processor UP 297 | Capture probe UP 298 | ``` 299 | 300 | ### Upgrade 301 | 302 | ```sh 303 | sudo service docker stop 304 | ebu_list_ctl upgrade 305 | sudo service docker start 306 | ebu_list_ctl status 307 | ``` 308 | 309 | ## Storage 310 | 311 | Use M.2 nvme SDD as a buffer for pcap file capture. 312 | 313 | ```sh 314 | lsblk | grep nvme # find the one that IS NOT used for OS 315 | fdisk /dev/nvme0n1 # create new partition 'n', primary 'p', default size, save 'w' 316 | mkfs.ext4 -F /dev/nvme0n1p1 317 | mkdir -p /media/buffer 318 | mount /dev/nvme0n1p1 /media/buffer 319 | chown -R ebulist:ebulist /media/buffer/* 320 | ``` 321 | TODO tune block sizes 322 | 323 | For persistent mounting, add this line in `/etc/fstab`: 324 | 325 | ``` 326 | UUID=nvme_UUID /media/buffer ext4 defaults 0 0 327 | ``` 328 | 329 | Determine write throughput for a given drive: 330 | 331 | ``` 332 | dd if=/dev/zero of=/media/buffer/zero.img bs=1G count=1 oflag=dsync 333 | ``` 334 | 335 | | Drive | FS | W speed MB/s | 336 | |-------|----|--------------| 337 | | RAM | | 2,400 | 338 | | nvme0 | ext4 | 262 | 339 | | nvme1 | ext4 | 683 | 340 | | SSD | ext4, raid0 | 651 | 341 | 342 | To be tested: 343 | 344 | * nvme0 used as [bcache device](https://www.linux.com/tutorials/using-bcache-soup-your-sata-drives/) 345 | * Fusion IO card (spec: 900 MB/s) 346 | 347 | ## TODO: 348 | 349 | * import ebu-list-sdk and query versions in `status` cmd 350 | * add internet access in `status` cmd 351 | -------------------------------------------------------------------------------- /ebu-list/capture_probe/capture.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const path = require('path'); 3 | const _ = require('lodash'); 4 | const fs = require('fs'); 5 | const util = require('util'); 6 | const recorder = require('./recorder'); 7 | const tcpdump = require('./tcpdump'); 8 | const { uploadFile } = require('./upload'); 9 | const unlink = util.promisify(fs.unlink); 10 | 11 | /////////////////////////////////////////////////////////////////////////////// 12 | 13 | const sleep = async (ms) => { 14 | return new Promise(resolve => { 15 | setTimeout(resolve, ms); 16 | }); 17 | }; 18 | 19 | const performCaptureAndIngest = async (globalConfig, workflowConfig) => { 20 | const endpoints = workflowConfig.senders 21 | // .map(sender => _.get(sender, ['sdp', 'streams[0]'], null)) // lodash is not doing this 22 | .map(sender => sender.sdp) 23 | .map(sdp => sdp.streams[0]); 24 | 25 | const captureFile = path.join(os.tmpdir(), workflowConfig.id + '.pcap'); 26 | 27 | const captureConfig = { 28 | endpoints: endpoints, 29 | durationMs: workflowConfig.durationMs, 30 | file: captureFile, 31 | }; 32 | 33 | if (globalConfig.recorder) { 34 | await recorder.runRecorder(globalConfig, captureConfig); 35 | } else if (globalConfig.tcpdump) { 36 | while (await tcpdump.runTcpdump(globalConfig, captureConfig) == 2) { 37 | await sleep(1000); 38 | } 39 | } 40 | 41 | try { 42 | await uploadFile( 43 | captureFile, 44 | workflowConfig.ingestPutUrl, 45 | workflowConfig.authorization, 46 | workflowConfig.filename 47 | ); 48 | } finally { 49 | await unlink(captureFile); 50 | } 51 | }; 52 | 53 | module.exports = { 54 | performCaptureAndIngest, 55 | }; 56 | -------------------------------------------------------------------------------- /ebu-list/capture_probe/tcpdump.js: -------------------------------------------------------------------------------- 1 | /* This script should replace ebu:pi-list:apps/capture_probe/tcpdump.js */ 2 | 3 | const child_process = require('child_process'); 4 | const { StringDecoder } = require('string_decoder'); 5 | const _ = require('lodash'); 6 | const logger = require('./logger'); 7 | 8 | const buildTcpdumpInfo = (globalConfig, captureOptions) => { 9 | const tcpdumpProgram = 'dpdk-capture.sh'; 10 | const tcpdumpFilter = captureOptions.endpoints 11 | ? `${captureOptions.endpoints.map(endpoint => { 12 | return endpoint.dstAddr ? 'dst ' + endpoint.dstAddr : ''; 13 | })}`.replace(/,/g, ' or ') 14 | : ''; 15 | var interfaces = _.get(globalConfig, ['tcpdump', 'interfaces']).split(','); 16 | 17 | if (interfaces.length == 0) { 18 | logger('live').info('no interface for capure'); 19 | return; 20 | } 21 | var pos = 0; 22 | while (pos < interfaces.length) { 23 | interfaces.splice(pos, 0, '-i'); 24 | pos += 2; 25 | } 26 | 27 | const tcpdumpArguments = interfaces.concat( [ 28 | '-w', 29 | captureOptions.file, 30 | '-G', 31 | (captureOptions.durationMs/1000).toString(), 32 | '-W', 33 | '1', 34 | tcpdumpFilter, 35 | ]); 36 | console.log(tcpdumpArguments); 37 | 38 | return { 39 | program: tcpdumpProgram, 40 | arguments: tcpdumpArguments, 41 | options: {}, 42 | }; 43 | }; 44 | 45 | const buildSubscriberInfo = (globalConfig, captureOptions) => { 46 | const binPath = _.get(globalConfig, ['list', 'bin']); 47 | 48 | if (!binPath) { 49 | throw new Error( 50 | `Invalid global configuration. list.bin not found: ${JSON.stringify(globalConfig)}` 51 | ); 52 | } 53 | 54 | const program = `${binPath}/subscribe_to`; 55 | 56 | const interfaceName = _.get(globalConfig, ['tcpdump', 'interface']); 57 | 58 | const addresses = captureOptions.endpoints.map(endpoint => endpoint.dstAddr); 59 | const groups = addresses.map(a => ["-g", a.toString()]); 60 | const gargs = groups.reduce((acc, val) => acc.concat(val), []); // TODO: flat() in node.js 11 61 | console.log("gargs"); 62 | console.dir(gargs); 63 | 64 | const arguments = [ 65 | interfaceName, 66 | ...gargs 67 | ]; 68 | 69 | return { 70 | program: program, 71 | arguments: arguments, 72 | options: {}, 73 | }; 74 | }; 75 | 76 | // Returns a promise 77 | const runTcpdump = async (globalConfig, captureOptions) => { 78 | 79 | const tcpdump = buildTcpdumpInfo(globalConfig, captureOptions); 80 | 81 | return new Promise((resolve, reject) => { 82 | logger('live').info( 83 | `command line: ${tcpdump.program} ${tcpdump.arguments.join(' ')}` 84 | ); 85 | 86 | const tcpdumpProcess = child_process.spawn( 87 | tcpdump.program, 88 | tcpdump.arguments, 89 | tcpdump.options 90 | ); 91 | 92 | const tcpdumpOutput = []; 93 | const decoder = new StringDecoder('utf8'); 94 | const appendToOutput = data => { 95 | tcpdumpOutput.push(decoder.write(data)); 96 | }; 97 | 98 | tcpdumpProcess.on('error', err => { 99 | logger('live').error(`error during capture:, ${err}`); 100 | }); 101 | 102 | tcpdumpProcess.stdout.on('data', appendToOutput); 103 | tcpdumpProcess.stderr.on('data', appendToOutput); 104 | 105 | let timer = null; 106 | 107 | tcpdumpProcess.on('close', code => { 108 | logger('live').info(`child process exited with code ${code}`); 109 | 110 | const stdout = tcpdumpOutput.join('\n'); 111 | 112 | logger('live').info(stdout); 113 | 114 | if (timer) { 115 | clearTimeout(timer); 116 | } 117 | 118 | if (killed) { 119 | logger('live').error('killed'); 120 | resolve(0); 121 | return; 122 | } 123 | 124 | if (code == null || code !== 0) { 125 | const message = `dpdk-capture failed with code: ${code}`; 126 | logger('live').error(message); 127 | if (code == 2) { /* retry */ 128 | resolve(code); 129 | } else { 130 | reject(new Error(message)); 131 | } 132 | return; 133 | } 134 | 135 | resolve(0); 136 | }); 137 | 138 | let killed = false; 139 | const onTimeout = () => { 140 | killed = true; 141 | //tcpdumpProcess.kill(); 142 | }; 143 | 144 | timer = setTimeout(onTimeout, captureOptions.durationMs * 2); 145 | }); 146 | }; 147 | 148 | module.exports = { 149 | runTcpdump, 150 | }; 151 | -------------------------------------------------------------------------------- /ebu-list/ebulist-probe.init: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | ### BEGIN INIT INFO 3 | # Provides: st2110 4 | # Required-Start: $time $network $local_fs $syslog 5 | # Required-Stop: 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 8 | # Short-Description: Start EBU-LIST Network Probe 9 | ### END INIT INFO 10 | # This header allows systemd to create a service. 11 | 12 | log_st2110() 13 | { 14 | logger -t st2110 "$@" 15 | } 16 | 17 | ST2110_CONF_FILE=/etc/st2110.conf 18 | if [ -f $ST2110_CONF_FILE ]; then 19 | . $ST2110_CONF_FILE 20 | fi 21 | 22 | start_probe() 23 | { 24 | log_st2110 "Start dpdk probe" 25 | smcrouted 26 | 27 | ifaces="$MEDIA_IFACE_0" 28 | if [ ! -z $MEDIA_IFACE_1 ]; then 29 | ifaces="$ifaces,$MEDIA_IFACE_1" 30 | fi 31 | 32 | echo 1000000000 > /proc/sys/kernel/shmmax 33 | echo 800 > /proc/sys/vm/nr_hugepages 34 | 35 | cd $LIST_PATH/apps/capture_probe/ 36 | cat > ./config-dpdk.yml << EOF 37 | probe: 38 | id: $(uuidgen) 39 | label: "LIST capture probe ${ifaces}: $(cat /etc/hostname)" 40 | rabbitmq: 41 | hostname: localhost 42 | port: 5672 43 | capture: 44 | engine: dpdk 45 | interfaces: ${ifaces} 46 | destination: ${LIST_DATA_FOLDER}/pcap 47 | EOF 48 | 49 | while ! netstat -lptn | grep -q ":5672"; do 50 | log_st2110 "wait mqtt port to be open" 51 | sleep 1 52 | done 53 | 54 | node ./server.js ./config-dpdk.yml 2>&1 > /tmp/list-dpdk.log & 55 | } 56 | 57 | stop_probe() { 58 | log_st2110 "Stop dpdk probe" 59 | smcroutectl kill 60 | pid=$(ps aux | grep "[n]ode.*dpdk.*yml" | tr -s ' ' | cut -d ' ' -f2) 61 | if [ ! -z $pid ]; then kill -9 $pid; fi 62 | } 63 | 64 | log_system(){ 65 | journalctl -xef -n 1000 | grep 2110 66 | } 67 | 68 | usage() 69 | { 70 | echo "Usage: $0 {start|stop}" 71 | } 72 | 73 | case "$1" in 74 | start) 75 | start_probe 76 | ;; 77 | stop) 78 | stop_probe 79 | ;; 80 | *) 81 | usage 82 | exit 1 83 | ;; 84 | esac 85 | 86 | exit 0 87 | -------------------------------------------------------------------------------- /ebu-list/ebulist-probe.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=EBU LIST Network Probe 3 | After=ebulist.service 4 | Requires=ebulist.service 5 | 6 | [Service] 7 | Type=oneshot 8 | RemainAfterExit=yes 9 | EnvironmentFile=/etc/st2110.conf 10 | ExecStart=/etc/init.d/ebulist-probe start 11 | ExecStop=/etc/init.d/ebulist-probe stop 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /ebu-list/ebulist.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=EBU LIST 3 | After=docker.service st2110.service ptp.service 4 | Requires=docker.service st2110.service ptp.service 5 | 6 | [Service] 7 | Type=simple 8 | RemainAfterExit=yes 9 | EnvironmentFile=/etc/st2110.conf 10 | ExecStart=/bin/bash -c "ebu_list_ctl -f start" 11 | ExecStop=/bin/bash -c "ebu_list_ctl stop" 12 | Restart=always 13 | RestartSec=10 14 | User=ebulist 15 | Group=ebulist 16 | 17 | [Install] 18 | WantedBy=multi-user.target 19 | -------------------------------------------------------------------------------- /ebu-list/freerun.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ST2110_CONF_FILE=/etc/st2110.conf 4 | if [ -f $ST2110_CONF_FILE ]; then 5 | . $ST2110_CONF_FILE 6 | fi 7 | MGMT_IP=$(ip addr show $MGMT_IFACE | tr -s ' ' | sed -n 's/ inet \(.*\)\/.*/\1/p') 8 | 9 | list_server=http://$MGMT_IP 10 | list_user=asd@asd.com 11 | list_pass=asd 12 | logname_prefix=/tmp/list_freerun_$(date | tr ' ' '_') 13 | duration=60 14 | 15 | mcast=225.0.0.1 #put your comma-separated stuff here 16 | 17 | cd $LIST_PATH/third_party/ebu-list-sdk/demos/ 18 | #npm run start -- live-capture -b $list_server -u $list_user -p $list_pass -m $mcast 19 | npm run start -- live-capture -f -b $list_server -u $list_user -p $list_pass -m $mcast -d $duration 2>${logname_prefix}_err1.txt | tee ${logname_prefix}_out1.txt & 20 | #sleep $duration 21 | #npm run start -- live-capture -f -b $list_server -u $list_user -p $list_pass -m $mcast -d $duration 2>${logname_prefix}_err2.txt | tee ${logname_prefix}_out2.txt & 22 | -------------------------------------------------------------------------------- /ebu-list/install.sh: -------------------------------------------------------------------------------- 1 | # !!! Don't execute this script directly !!! 2 | # It is imported in $TOP/install.sh 3 | 4 | install_list() 5 | { 6 | $PACKAGE_MANAGER install -y \ 7 | docker \ 8 | docker-compose 9 | 10 | if [ -f $ST2110_CONF_FILE ]; then 11 | source $ST2110_CONF_FILE 12 | else 13 | echo "Config should be installed first (install_config) and EDITED, exit." 14 | exit 1 15 | fi 16 | 17 | usermod -a -G adm $ST2110_USER # journalctl 18 | usermod -a -G pcap $ST2110_USER 19 | usermod -a -G docker $ST2110_USER 20 | 21 | ln -fs $TOP_DIR/ebu-list/ebu_list_ctl /usr/sbin/ebu_list_ctl 22 | 23 | # su $ST2110_USER -c "ebu_list_ctl install" 24 | cd /home/$ST2110_USER 25 | git clone https://github.com/ebu/pi-list.git $LIST_PATH 26 | cd $LIST_PATH 27 | git submodule update --init --recursive 28 | ./scripts/setup_build_env.sh 29 | 30 | # dev mode means build application from source 31 | if [ $LIST_DEV = "true" ]; then 32 | ./scripts/deploy/deploy.sh 33 | else 34 | # whereas non dev mode means install from public docker image 35 | cd $LIST_PATH/docs 36 | docker-compose pull 37 | 38 | # but still need to build node apps to run the capture probe 39 | cd $LIST_PATH/ 40 | ./scripts/build_node.sh 41 | fi 42 | 43 | chown -R $ST2110_USER:$ST2110_USER $LIST_PATH 44 | 45 | cp $TOP_DIR/ebu-list/ebulist.service /lib/systemd/system 46 | install -m 755 $TOP_DIR/ebu-list/ebulist-probe.init /etc/init.d/ebulist-probe 47 | cp $TOP_DIR/ebu-list/ebulist-probe.service /lib/systemd/system 48 | 49 | systemctl daemon-reload 50 | systemctl enable ebulist 51 | systemctl enable ebulist-probe 52 | } 53 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Compile & Install everything for FFMPEG transcoding 4 | # and tcpdump capturing and EBU-LIST 5 | 6 | set -euo pipefail 7 | 8 | usage(){ 9 | echo "Usage: $0
10 | sections are: 11 | * common: compile tools, network utilities, config 12 | * ptp: linuxptp 13 | * transcoder: ffmpeg, x264, mp3 and other codecs 14 | * capture: dpdk-based capture engine for Mellanox ConnectX-5 15 | * ebulist: EBU-LIST pcap analyzer, NOT tested for a while 16 | * nmos: Sony nmos-cpp node and scripts for SDP patching 17 | 18 | Regardless of your setup, please install 'common' section first. 19 | " 20 | } 21 | 22 | if [ ! $UID -eq 0 ]; then 23 | echo "Not root, exit." 24 | exit 1 25 | fi 26 | if [ $# -eq 0 ]; then 27 | echo "Missing args." 28 | usage 29 | exit 1 30 | fi 31 | 32 | TOP_DIR=$(dirname $(readlink -f $0)) 33 | ST2110_CONF_FILE=/etc/st2110.conf 34 | OS=$(cat "/etc/os-release" | sed -n 's/^ID=\(.*\)/\1/p' | tr -d '"') 35 | echo "OS: $OS detected" 36 | 37 | if [ $OS = "debian" -o $OS = "ubuntu" ]; then 38 | PACKAGE_MANAGER=apt 39 | elif [ $OS = "centos" -o $OS = "redhat" ]; then 40 | PACKAGE_MANAGER=yum 41 | else 42 | echo "OS not supported." 43 | exit 1 44 | fi 45 | 46 | export LANG=en_US.utf8 \ 47 | LC_ALL=en_US.utf8 \ 48 | PREFIX=/usr/local \ 49 | 50 | export PKG_CONFIG_PATH=${PREFIX}/lib/pkgconfig \ 51 | 52 | echo "${PREFIX}/lib" >/etc/ld.so.conf.d/libc.conf 53 | 54 | source $TOP_DIR/capture/install.sh 55 | source $TOP_DIR/ebu-list/install.sh 56 | source $TOP_DIR/nmos/install.sh 57 | source $TOP_DIR/transcoder/install.sh 58 | source $TOP_DIR/ptp/install.sh 59 | 60 | install_common_tools() 61 | { 62 | $PACKAGE_MANAGER -y update && $PACKAGE_MANAGER install -y \ 63 | autoconf \ 64 | automake \ 65 | bzip2 \ 66 | cmake \ 67 | lldpd \ 68 | ethtool \ 69 | gcc \ 70 | git \ 71 | jq \ 72 | libtool \ 73 | make \ 74 | net-tools \ 75 | patch \ 76 | perl \ 77 | python-is-python3 \ 78 | sshpass \ 79 | tar \ 80 | tcpdump \ 81 | tmux \ 82 | wget \ 83 | wireshark-common 84 | 85 | if [ $PACKAGE_MANAGER = "yum" ]; then 86 | $PACKAGE_MANAGER -y update && $PACKAGE_MANAGER install -y \ 87 | nc \ 88 | gcc-c++ \ 89 | openssl-devel \ 90 | which \ 91 | zlib-devel 92 | else 93 | $PACKAGE_MANAGER -y update && $PACKAGE_MANAGER install -y \ 94 | libssl-dev \ 95 | g++ \ 96 | zlib1g-dev 97 | fi 98 | 99 | # rigth capabilities in order to use tcpdump, ip, iptables without sudo 100 | groupadd -f pcap 101 | for p in tcpdump ip iptables; do 102 | bin=$(readlink -f $(which $p)) 103 | chgrp pcap $bin 104 | setcap cap_net_raw,cap_net_admin=eip $bin 105 | done 106 | } 107 | 108 | install_dev_tools() 109 | { 110 | if [ $PACKAGE_MANAGER = "yum" ]; then 111 | wget dl.fedoraproject.org/pub/epel/7/x86_64/Packages/e/epel-release-7-11.noarch.rpm 112 | rpm -ihv epel-release-7-11.noarch.rpm 113 | fi 114 | 115 | $PACKAGE_MANAGER -y install \ 116 | htop \ 117 | nload \ 118 | vim \ 119 | tig \ 120 | psmisc 121 | } 122 | 123 | install_config() 124 | { 125 | if [ ! -f $ST2110_CONF_FILE ]; then 126 | install -m 644 $TOP_DIR/config/st2110.conf $ST2110_CONF_FILE 127 | echo "************************************************" 128 | echo "Default config installed: $ST2110_CONF_FILE" 129 | echo "If necessary, change ST2110_USER inside and create user on this system." 130 | echo "Then run again." 131 | echo "************************************************" 132 | fi 133 | source $ST2110_CONF_FILE 134 | 135 | if [ ! -d /home/$ST2110_USER ]; then 136 | echo "************************************************" 137 | echo "/home/$ST2110_USER doesn't exist. Verify ST2110_USER in $ST2110_CONF_FILE" 138 | echo "and add the appropriate user on this system if necessary." 139 | echo "Then run again." 140 | echo "************************************************" 141 | exit -1 142 | fi 143 | 144 | install -m 666 $TOP_DIR/config/st2110.bashrc /home/$ST2110_USER/ 145 | if ! grep -q 2110 /home/$ST2110_USER/.bashrc; then 146 | echo "source /home/$ST2110_USER/st2110.bashrc" >> /home/$ST2110_USER/.bashrc 147 | fi 148 | 149 | install -m 755 $TOP_DIR/config/st2110.init /etc/init.d/st2110 150 | update-rc.d st2110 defaults 151 | systemctl enable st2110 152 | } 153 | 154 | #set -x 155 | 156 | case "$1" in 157 | common) 158 | install_common_tools 159 | install_dev_tools 160 | install_config 161 | ;; 162 | ptp) 163 | install_ptp 164 | ;; 165 | transcoder) 166 | install_transcoder 167 | ;; 168 | capture) 169 | install_capture 170 | ;; 171 | ebulist) 172 | install_list 173 | ;; 174 | nmos) 175 | install_nmos 176 | ;; 177 | *) 178 | usage 179 | ;; 180 | esac 181 | set +x 182 | -------------------------------------------------------------------------------- /nmos/Dockerfile: -------------------------------------------------------------------------------- 1 | # Virtual NMOS node/registry image 2 | # 3 | # docker build command should be executed from top directory: 4 | # $ docker build -t centos/nmos:v0 -f ./nmos/Dockerfile . 5 | 6 | FROM centos:latest 7 | 8 | RUN adduser --uid 1000 --home /home/transcoder transcoder 9 | WORKDIR /home/transcoder/ 10 | 11 | RUN yum -y update && yum install -y git 12 | 13 | ADD . /home/transcoder/st2110_toolkit/ 14 | RUN source /home/transcoder/st2110_toolkit/install.sh && \ 15 | install_common_tools && \ 16 | install_cmake && \ 17 | install_boost && \ 18 | install_mdns && \ 19 | install_cpprest && \ 20 | install_cppnode 21 | -------------------------------------------------------------------------------- /nmos/README.md: -------------------------------------------------------------------------------- 1 | # NMOS tools 2 | 3 | ## Overview 4 | 5 | The idea is to combine a NMOS node instance and a media application to 6 | build a pure software receiver controlled by IS-05. 7 | 8 | * implementation for NMOS virtual node is [Sony nmos-cpp](https://github.com/sony/nmos-cpp). Provided Dockerfile generates a Centos-based image dedicated to this node. 9 | * the media application is our present FFmpeg transcoder 10 | 11 | As sudo, from top directory: 12 | 13 | ```sh 14 | ./install.sh nmos 15 | ``` 16 | 17 | Config file is `/home/$USER/nmos.json`. Control the service with systemctl: 18 | 19 | ``` 20 | sudo systemctl status|start|stop nmos 21 | ``` 22 | 23 | ## Scripts 24 | 25 | * nmos_node.py: NMOS node class implementation with API calls 26 | * node_poller.py: 27 | - poll Rx status from a virtual node using connection API (IS-05) 28 | - on RX activation, pull SDPs and adapt for FFmpeg 29 | - start/stop FFmpeg-based transcoder 30 | * node_connection.py: establish a IS-05 connection between a sender and a receiver 31 | - fetche transport file of the 1st video and 1st audio flows of the sender device 32 | - pushe to the consport files to corresponding flows of the receiver device 33 | * node_controller.py: controls (start/stop, rx/tx) the nmos-cpp-node 34 | 35 | ## Execute: 36 | 37 | Run run Sony virtual node: 38 | 39 | ```sh 40 | ~/nmos-cpp/Development/build/nmos-cpp-node ~/st2110_toolkit/nmos/config/nmos-cpp-ffmpeg-mdns-config.json 41 | ``` 42 | 43 | Run the node poller on the same host: 44 | 45 | ```sh 46 | ~/st2110_toolkit/nmos/node_poller.py 47 | ``` 48 | 49 | Establish a connection from NMOS-capable sender: 50 | 51 | ```sh 52 | ~/st2110_toolkit/nmos/node_connection.py start 53 | ``` 54 | 55 | Disable the receiver node: 56 | 57 | ```sh 58 | ~/st2110_toolkit/nmos/node_controller.py rx stop 59 | ``` 60 | 61 | ## TODO 62 | 63 | * Merge node_controller in node_connection 64 | * Except from the poller , it's probably a better idea to re-use [nmos-testing](https://github.com/AMWA-TV/nmos-testing) 65 | 66 | ## Misc 67 | 68 | * Binding cpp-nmos connection API to port 80 needs sudo on Ubunutu 18. 69 | * be carefull of the IPs exposed by virtual node, it could be mgmt IP, the client can be confused 70 | * video SDP and audio SDP have to be combined into a single file to work with FFmpeg but it's not allow by ST2110. 71 | -------------------------------------------------------------------------------- /nmos/api_patch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | import sys 3 | import json 4 | import urllib2 5 | from nmos_node import * 6 | 7 | def usage(): 8 | print(""" 9 | api_path.py - this script polls the NOMS connection API of a 10 | \treceiver to get active connections and start ffmpeg transcoder to join 11 | \tthe same multicast group. 12 | 13 | Usage: 14 | \api_path.py 15 | 16 | """) 17 | 18 | def main(): 19 | if len(sys.argv) < 3: 20 | usage() 21 | return 22 | 23 | url = sys.argv[1] 24 | filename = sys.argv[2] 25 | with open(filename, 'r') as f: 26 | patch = json.load(f) 27 | print(">>>") 28 | print(json.dumps(patch, indent=1)) 29 | content = patch_url(url, patch) 30 | print("<<<") 31 | print(json.dumps(content, indent=1)) 32 | 33 | if __name__ == "__main__": 34 | main() 35 | -------------------------------------------------------------------------------- /nmos/connection_loop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from nmos_node import NmosNode 5 | import time 6 | from datetime import datetime 7 | 8 | def usage(): 9 | print(""" 10 | connection_loop.py - this script connects a sender to a receiver 11 | node based on connection API (IS-05). 12 | 13 | Usage: 14 | \tconnection_loop.py 15 | 16 | """) 17 | 18 | def active(tx, rx, state): 19 | tx.activate_all(tx_ch, state) 20 | rx.activate_all(rx_ch, state) 21 | 22 | def patch_flow(name, tx, rx, tx_ch, rx_ch): 23 | connection_log(' {}[{}] -> {}[{}]'.format(name, tx_ch, name, rx_ch)) 24 | rx.set_connection_sdp(rx.connections[rx_ch][name]['id'], tx.connections[tx_ch][name]['id'], tx.get_sdp(tx.connections[tx_ch][name]['id'])) 25 | #TODO verify sdp 26 | 27 | def patch_channel(tx, rx, tx_ch, rx_ch): 28 | connection_log('{}[{}] -> {}[{}]'.format(tx.ip, tx_ch, rx.ip, rx_ch)) 29 | patch_flow('vid ', tx, rx, tx_ch, rx_ch) 30 | patch_flow('aud1', tx, rx, tx_ch, rx_ch) 31 | patch_flow('aud1', tx, rx, tx_ch, rx_ch) 32 | patch_flow('anc ', tx, rx, tx_ch, rx_ch) 33 | 34 | def connection_log(msg): 35 | print("[connection] " + msg) 36 | 37 | def main(): 38 | if len(sys.argv) < 3: 39 | usage() 40 | return 41 | 42 | tx = NmosNode(ip = sys.argv[1], type = 'tx') 43 | rx = NmosNode(ip = sys.argv[2], type = 'rx') 44 | 45 | #TODO: add counter and timestampt 46 | while True: 47 | now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") 48 | 49 | #TODO activate 50 | connection_log('*' * 72 + now) 51 | tx.update_connections() 52 | rx.update_connections() 53 | patch_channel(tx,rx,0,0) 54 | patch_channel(tx,rx,1,1) 55 | time.sleep(2) 56 | patch_channel(tx,rx,0,1) 57 | patch_channel(tx,rx,1,0) 58 | time.sleep(2) 59 | 60 | if __name__ == "__main__": 61 | main() 62 | connection_log("Exit.") 63 | -------------------------------------------------------------------------------- /nmos/curl-format.txt: -------------------------------------------------------------------------------- 1 | ime_namelookup: %{time_namelookup}s\n 2 | time_connect: %{time_connect}s\n 3 | time_appconnect: %{time_appconnect}s\n 4 | time_pretransfer: %{time_pretransfer}s\n 5 | time_redirect: %{time_redirect}s\n 6 | time_starttransfer: %{time_starttransfer}s\n 7 | ----------\n 8 | time_total: %{time_total}s\n 9 | -------------------------------------------------------------------------------- /nmos/get_sdp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | usage() 4 | { 5 | echo "Get all SDP file through NMOS Connection API for a given 6 | 'IP:port'. Works well only in both IS-04 and IS-05 run on the same 7 | port. 8 | $0 [-w] r|s IP[:port] 9 | -w write to file, write to stdout by default 10 | r|s get receivers OR senders 11 | IP node IP 12 | " 13 | } 14 | 15 | if [ $1 == "-w" ]; then 16 | WRITE=true 17 | shift 18 | fi 19 | 20 | direction='' 21 | if [ $1 == "r" ]; then 22 | direction="receivers" 23 | elif [ $1 == "s" ]; then 24 | direction="senders" 25 | else 26 | usage 27 | exit 1 28 | fi 29 | shift 30 | 31 | if [ -z $1 ]; then 32 | usage 33 | exit 1 34 | fi 35 | 36 | IP=$1 37 | connection_base_url="http:/$IP/x-nmos/connection/v1.1/single/$direction/" 38 | node_base_url="http:/$IP/x-nmos/node/v1.2/$direction/" 39 | 40 | echo "Get NMOS SDP @ $connection_base_url" 41 | curl $connection_base_url 2>/dev/null 42 | list=$(curl $connection_base_url 2>/dev/null | jq | sed -n 's/^ "\(.*\)".*/\1/p') # remove leading spaces 43 | 44 | for id in $list; do 45 | url=${connection_base_url}${id}active 46 | echo "---------------------------------" 47 | echo $url 48 | id_no_slash=$(echo $id | sed 's;\/;;') # remove '/' 49 | curl $node_base_url 2>/dev/null | jq ".[] | select( .id == \"$id_no_slash\").label" 50 | curl $node_base_url 2>/dev/null | jq ".[] | select( .id == \"$id_no_slash\").caps.media_types" 51 | sdp=$(curl $url 2>/dev/null ) 52 | 53 | if [ ! -n $WRITE ]; then 54 | sdpfile=$(echo "$sdp" | sed -n 's/^s=\(.*\)/\1/p' | sed 's/\r$//').sdp 55 | echo "New sdp file: $sdpfile" 56 | echo "$sdp" > ${sdpfile} 57 | else 58 | echo "$sdp" 59 | fi 60 | done 61 | -------------------------------------------------------------------------------- /nmos/install.sh: -------------------------------------------------------------------------------- 1 | # !!! Don't execute this script directly !!! 2 | # It is imported in $TOP/install.sh 3 | 4 | export CMAKE_VERSION=3.21.1 \ 5 | BOOST_VERSION=1.67.0 \ 6 | MDNS_VERSION=878.30.4 \ 7 | REST_VERSION=2.10.11 8 | 9 | install_cmake() 10 | { 11 | echo "Installing CMake" 12 | DIR=$(mktemp -d) 13 | cd $DIR/ 14 | wget --no-check-certificate https://cmake.org/files/v3.21/cmake-$CMAKE_VERSION.tar.gz 15 | tar xvf cmake-$CMAKE_VERSION.tar.gz 16 | cd $DIR/cmake-$CMAKE_VERSION 17 | ./bootstrap 18 | make 19 | make install 20 | rm -rf $DIR 21 | } 22 | 23 | install_conan() 24 | { 25 | echo "Installing conan" 26 | pip install conan==v1.45 27 | } 28 | 29 | install_boost() 30 | { 31 | echo "Installing Boost" 32 | DIR=$(mktemp -d) 33 | cd $DIR/ 34 | boost_version=$(echo $BOOST_VERSION | tr '.' '_') 35 | wget --no-check-certificate https://dl.bintray.com/boostorg/release/$BOOST_VERSION/source/boost_$boost_version.tar.gz 36 | tar xvf boost_$boost_version.tar.gz 37 | cd $DIR/boost_$boost_version 38 | ./bootstrap.sh --with-libraries=date_time,regex,system,thread,random,filesystem,chrono,atomic --prefix=$PREFIX 39 | ./b2 install 40 | rm -rf $DIR 41 | } 42 | 43 | install_mdns(){ 44 | ## You should use either Avahi or Apple mDNS - DO NOT use both 45 | echo "Installing mDNSResponder" 46 | wget --no-check-certificate https://opensource.apple.com/tarballs/mDNSResponder/mDNSResponder-$MDNS_VERSION.tar.gz 47 | tar xvf mDNSResponder-$MDNS_VERSION.tar.gz 48 | 49 | # patch to make mdnsd work with unicast DNS 50 | wget https://raw.githubusercontent.com/sony/nmos-cpp/master/Development/third_party/mDNSResponder/poll-rather-than-select.patch 51 | patch -d mDNSResponder-$MDNS_VERSION/ -p1 < poll-rather-than-select.patch 52 | wget https://raw.githubusercontent.com/sony/nmos-cpp/master/Development/third_party/mDNSResponder/unicast.patch 53 | patch -d mDNSResponder-$MDNS_VERSION/ -p1 < unicast.patch 54 | 55 | cd ./mDNSResponder-$MDNS_VERSION/mDNSPosix 56 | set HAVE_IPV6=0 57 | #TODO: put that in $PREFIX 58 | make os=linux 59 | make os=linux install 60 | #rm -rf $DIR 61 | } 62 | 63 | install_cpprest() 64 | { 65 | echo "Installing C++ REST" 66 | DIR=$(mktemp -d) 67 | cd $DIR/ 68 | git clone --recurse-submodules --branch v$REST_VERSION https://github.com/Microsoft/cpprestsdk 69 | mkdir cpprestsdk/Release/build 70 | cd cpprestsdk/Release/build 71 | 72 | cmake .. \ 73 | -DCMAKE_BUILD_TYPE:STRING="Release" \ 74 | -DWERROR:BOOL="0" 75 | make 76 | make install 77 | cp -rf ../libs/websocketpp/websocketpp/ $PREFIX/include/ 78 | 79 | rm -rf $DIR 80 | } 81 | 82 | install_cppnode() 83 | { 84 | echo "Installing Sony nmos-cpp" 85 | git clone https://github.com/sony/nmos-cpp.git 86 | mkdir ./nmos-cpp/Development/build 87 | cd ./nmos-cpp/Development/build 88 | 89 | cmake .. \ 90 | -G "Unix Makefiles" \ 91 | -DCMAKE_CONFIGURATION_TYPES:STRING="Debug" \ 92 | -DBoost_USE_STATIC_LIBS:BOOL="1" \ 93 | -DCMAKE_CXX_FLAGS="-fpermissive" \ 94 | -DWEBSOCKETPP_INCLUDE_DIR:PATH="$PREFIX/include/websocketpp" 95 | 96 | make 97 | install -m 755 ./nmos-cpp-node ./nmos-cpp-registry ./nmos-cpp-test $PREFIX/bin 98 | } 99 | 100 | install_cppnode_example() 101 | { 102 | echo "Installing node example based on nmos-cpp lib" 103 | mkdir dev 104 | cd dev 105 | git clone https://github.com:pkeroulas/nmos-cpp-examples 106 | mkdir ./nmos-cpp-examples/build 107 | cd ./nmos-cpp-examples/build 108 | cmake .. -DCMAKE_BUILD_TYPE:STRING="Release" 109 | make 110 | ln -s ~/dev/nmos-cpp-examples/build/my-nmos-node/my-nmos-node ~/my-nmos-node 111 | cp ./nmos.json ~/my-nmos-node 112 | } 113 | 114 | install_nmos_init(){ 115 | install -m 755 ./nmos.init /etc/init.d/nmos 116 | update-rc.d nmos defaults 117 | systemctl enable nmos 118 | systemctl start nmos 119 | } 120 | 121 | install_nmos() { 122 | set -x 123 | install_cmake 124 | install_conan 125 | install_cppnode_example 126 | install_nmos_init 127 | set +x 128 | } 129 | -------------------------------------------------------------------------------- /nmos/multi_nmos_node_conf_gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script generates minimalistic conf files for nmos-cpp nodes 3 | # It also generates the docker-compose file. 4 | 5 | DESCRIPTION="CBC virt node " 6 | DOMAIN=nmos-tb.org 7 | DOCKERFILE=docker-compose.yml 8 | 9 | if [ -z $1 ]; then 10 | echo "Usage: 11 | $0 " 12 | exit 1 13 | else 14 | N=$1 15 | fi 16 | 17 | rm -rf node*.conf 18 | 19 | echo "version: '3.6' 20 | services:" > $DOCKERFILE 21 | 22 | for n in $(seq $N); do 23 | echo Gen conf for node \#$n 24 | 25 | port=$((8100 + $n)) 26 | conf=node$n.conf 27 | echo "{ 28 | \"http_port\": $port, 29 | \"logging_level\": 0, 30 | \"label\": \"$DESCRIPTION - $n\", 31 | \"description\": \"$DESCRIPTION - $n\", 32 | \"registry_version\": \"v1.3\", 33 | \"domain\": \"$DOMAIN\", 34 | \"query_paging_default\": 20 35 | }" > $conf 36 | 37 | echo " noms-virtnode-$n: 38 | image: rhastie/nmos-cpp:latest 39 | container_name: nmos-virtnode-$n 40 | hostname: cbc-nmos-virtnode-$n 41 | network_mode: \"host\" 42 | volumes: 43 | - \"./$conf:/home/node.json\" 44 | environment: 45 | - RUN_NODE=TRUE 46 | " >> $DOCKERFILE 47 | 48 | done 49 | 50 | echo ============ $DOCKERFILE =============== 51 | cat $DOCKERFILE 52 | 53 | echo Suggestion: \"docker-compose up\" 54 | -------------------------------------------------------------------------------- /nmos/nmos.init: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | ### BEGIN INIT INFO 3 | # Provides: nmos 4 | # Required-Start: $st2110 5 | # Required-Stop: 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 8 | # Short-Description: Start nmos-cpp-node 9 | ### END INIT INFO 10 | # This header allows systemd to create a service. 11 | 12 | # To enable the initscript on SYSV init system: 13 | # Copy to /etc/init.d/nmos with root ownership 14 | # $ update-rc.d nmos defaults 15 | # $ systemctl enable nmos 16 | # $ systemctl start nmos 17 | set -x 18 | log_nmos() 19 | { 20 | logger -t nmos "$@" 21 | } 22 | 23 | ST2110_CONF_FILE=/etc/st2110.conf 24 | if [ -f $ST2110_CONF_FILE ]; then 25 | . $ST2110_CONF_FILE 26 | fi 27 | 28 | NMOS_PATH=/home/$ST2110_USER 29 | NMOS_DAEMON=$NMOS_PATH/my-nmos-node 30 | NMOS_CONFIG=$NMOS_PATH/nmos.json 31 | NMOS_NODE_PID=/var/run/nmos.pid 32 | 33 | start_nmos() 34 | { 35 | log_nmos "Start nmos-node" 36 | start-stop-daemon --start --background --chuid $ST2110_USER -m --oknodo --pidfile $NMOS_NODE_PID --exec $NMOS_DAEMON -- $NMOS_CONFIG 37 | } 38 | 39 | stop_nmos() 40 | { 41 | log_nmos "Stop nmos-node" 42 | start-stop-daemon --stop --pidfile $NMOS_NODE_PID --oknodo 43 | } 44 | 45 | usage() 46 | { 47 | echo "Usage: $0 {start|stop}" 48 | } 49 | 50 | case "$1" in 51 | start) 52 | start_nmos 53 | ;; 54 | stop) 55 | stop_nmos 56 | ;; 57 | *) 58 | usage 59 | exit 1 60 | ;; 61 | esac 62 | 63 | exit 0 64 | -------------------------------------------------------------------------------- /nmos/nmos.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "ST2110 device", 3 | "description": "ST2110 device", 4 | "host_addresses": ["0.0.0.0"], 5 | "host_address": "0.0.0.0", 6 | "http_port": 8100, 7 | "registry_address": "0.0.0.0", 8 | "registration_port": 8000, 9 | "registry_version": "v1.3", 10 | "system_address": "0.0.0.0", 11 | "system_port": 8000, 12 | "system_version": "v1.0", 13 | "logging_level": 0, 14 | "error_log": "/tmp/nmos.err", 15 | "access_log": "/tmp/nmos.access", 16 | "how_many": 2, 17 | "smpte2022_7": true, 18 | "channel_count": 6, 19 | "seed_id": "ff69f8b6-afbd-2566-95d5-40a36ba03bef", 20 | "don't worry": "about trailing commas" 21 | } 22 | -------------------------------------------------------------------------------- /nmos/nmos_node.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | import json 3 | import urllib 4 | from urllib.request import urlopen 5 | import urllib.error as urlerror 6 | 7 | class NmosNode: 8 | def __init__(self, ip = '127.0.0.1', type = 'rx'): 9 | if not type == 'rx' and not type == 'tx': 10 | raise("Node must be type 'rx' or 'tx'") 11 | self.ip = ip 12 | self.type = 'receivers' if type == 'rx' else 'senders' 13 | self.channels = [0, 1] 14 | 15 | # get latest node version 16 | self.emsfp_version = self.get_from_url("http://" + self.ip + "/emsfp/node/")[-1] 17 | self.node_version = self.get_from_url("http://" + self.ip + "/x-nmos/node/")[-1] 18 | self.connection_version = self.get_from_url("http://" + self.ip + "/x-nmos/connection/")[-1] 19 | 20 | connection_ids = self.get_connection_ids() 21 | channel_length = int(len(connection_ids) / 2) 22 | self.connections = [ { 'vid ': {}, 'aud1': {} , 'aud2': {}, 'aud3': {}, 'aud4': {}, 'anc ' : {} }, \ 23 | { 'vid ': {}, 'aud1': {} , 'aud2': {}, 'aud3': {}, 'aud4': {}, 'anc ' : {} } ] 24 | # Embrionix EmSFP specific mapping 25 | for i in self.channels: 26 | self.connections[i]['vid '] = self.fill_connection(connection_ids[i*channel_length+0]) 27 | self.connections[i]['aud1'] = self.fill_connection(connection_ids[i*channel_length+1]) 28 | self.connections[i]['aud2'] = self.fill_connection(connection_ids[i*channel_length+2]) 29 | #self.connections[i]['aud3'] = self.fill_connection(connection_ids[i*channel_length+3]) 30 | #self.connections[i]['aud4'] = self.fill_connection(connection_ids[i*channel_length+4]) 31 | self.connections[i]['anc '] = self.fill_connection(connection_ids[i*channel_length+channel_length-1]) 32 | self.log(json.dumps(self.connections, indent=2)) 33 | 34 | def log(self, msg): 35 | print(" [node]["+self.ip +"]["+self.type+"]:" + str(msg)) 36 | 37 | def get_emsfp_connection_url(self): 38 | return "http://" + self.ip + "/emsfp/node/"+ self.emsfp_version + self.type + "/" 39 | 40 | def get_emsfp_flow_url(self): 41 | return "http://" + self.ip + "/emsfp/node/"+ self.emsfp_version + "/flows/" 42 | 43 | def get_connection_url(self): 44 | return "http://" + self.ip + "/x-nmos/connection/" + self.connection_version + "single/" + self.type + "/" 45 | 46 | def get_node_url(self): 47 | return "http://" + self.ip + "/x-nmos/node/" + self.node_version + self.type + "/" 48 | 49 | def get_connection_ids(self): 50 | res = [] 51 | try: 52 | res = self.get_from_url(self.get_connection_url()) 53 | except Exception as e: 54 | self.log(e) 55 | self.log("Unable to get tx id for ip:" + str(self.ip)) 56 | return [i.replace('/','') for i in res] 57 | 58 | def fill_connection(self, connection): 59 | emsfp_connection = self.get_from_url(self.get_emsfp_connection_url()+connection+'/') 60 | flow_id = '' if 'flow_id' not in emsfp_connection.keys() else emsfp_connection['flow_id'] 61 | media = '' if flow_id == '' else self.get_from_url(self.get_emsfp_flow_url()+flow_id[0])['format']['format_type'] # red only 62 | return { 'id': connection, 'flow' : flow_id, 'media': media, 'pkt_count' : ''} 63 | 64 | def update_connections(self): 65 | for i in self.channels: 66 | for j in self.connections[i]: 67 | if self.connections[i][j]: 68 | pkt_count = self.get_pkt_count(self.connections[i][j]['flow'][0]) # just red 69 | if pkt_count != self.connections[i][j]['pkt_count']: 70 | self.connections[i][j]['pkt_count'] = pkt_count 71 | self.log('ch[{}] - {} - pkt:{}'.format(i,j,pkt_count)) 72 | else: 73 | self.log('ch[{}] - {} - pkt:{}'.format(i,j,'unchanged!!!!!!!!!!!!!!')) 74 | 75 | def get_pkt_count(self, flow_id): 76 | try: 77 | res = self.get_from_url(self.get_emsfp_flow_url()+flow_id+'/') 78 | return res['network']['pkt_cnt'] 79 | except Exception as e: 80 | self.log(e) 81 | self.log("Unable to get tx id for ip:" + str(self.ip)) 82 | 83 | def get_sdp(self, id): 84 | res = 'unknown' 85 | url = self.get_node_url() 86 | try: 87 | for slot in self.get_from_url(url): 88 | if slot['id'] == id: 89 | sdp_url = slot['manifest_href'] 90 | res = self.get_from_url(sdp_url) 91 | except Exception as e: 92 | self.log(e) 93 | self.log("Unable to get sdp from url: " + url + id) 94 | return str(res) 95 | 96 | def activate_all(self, active): 97 | ids = self.get_ids() 98 | for connection_id in ids: 99 | self.activate(active, connection_id) 100 | 101 | def activate_ch(self, ch, active): 102 | for id in self.connections[ch]: 103 | self.activate(active, id) 104 | 105 | def activate(self, active, id): 106 | url = self.get_connection_url() + str(id) + "/staged/" 107 | patch = {"activation":{"mode":"activate_immediate"},"master_enable":active} 108 | self.patch_url(url, patch) 109 | self.log(self.type + "/" + str(id) + ": active=" + str(active)) 110 | 111 | def get_connection_status(self, id): 112 | res = True 113 | url = self.get_connection_url() + str(id) + "/active/" 114 | try: 115 | connection = self.get_from_url(url) 116 | if not connection['master_enable'] or not connection['activation']['mode']: 117 | res = False 118 | except Exception as e: 119 | self.log(e) 120 | self.log("Unable to get connection status for url:" + url) 121 | return res 122 | 123 | def get_connection_sdp(self, id): 124 | res = None 125 | url = self.get_connection_url() + str(id) + "/active/" 126 | try: 127 | connection = self.get_from_url(url) 128 | #self.log(json.dumps(connection, indent=2)) 129 | res = connection['transport_file']['data'] 130 | except Exception as e: 131 | self.log(e) 132 | self.log("Unable to set connection status for url:" + url) 133 | return res 134 | 135 | def set_connection_sdp(self, rx_id, tx_id, sdp): 136 | url = self.get_connection_url() + str(rx_id) + "/staged/" 137 | patch = {"sender_id":tx_id,"transport_file":{"data":str(sdp),"type":"application/sdp"}} 138 | try: 139 | self.patch_url(url, patch) 140 | except Exception as e: 141 | self.log(e) 142 | self.log("Unable to set connection status for id:" + url + id) 143 | #self.log(self.type + "/" + str(rx_id) + " sdp=" + str(sdp)[:200] + "...") 144 | 145 | def get_from_url(self, url): 146 | res = None 147 | try: 148 | content = urlopen(url, timeout=1).read() 149 | try: 150 | res = json.loads(content) 151 | except: 152 | res = content 153 | except urlerror.HTTPError as e: 154 | self.log(e.code) 155 | self.log(e.url) 156 | return e.read() 157 | except Exception as e: 158 | self.log(e) 159 | return res 160 | 161 | def patch_url(self, url, patch): 162 | try: 163 | #self.log("patch url:" + url) 164 | #self.log(json.dumps(patch, indent = 1)) 165 | data = urllib.parse.urlencode(patch).encode("utf-8") 166 | request = urllib.request.Request(url, data=data, method='PATCH') 167 | request.add_header('Content-Type', 'application/json') 168 | content = urlopen(request).read() 169 | return json.loads(content) 170 | except urlerror.HTTPError as e: 171 | return e.read() 172 | except Exception as e: 173 | self.log(e) 174 | return None 175 | -------------------------------------------------------------------------------- /nmos/nmos_st2110_monitor_ctl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | ST2110_CONF_FILE=/etc/st2110.conf 4 | if [ ! -f $ST2110_CONF_FILE ]; then 5 | echo "Config file is missing" 6 | return 7 | fi 8 | source $ST2110_CONF_FILE 9 | 10 | NMOS_CPP_PATH=/home/$ST2110_USER/nmos-cpp/Development/build 11 | NMOS_ST2110_PATH=/home/$ST2110_USER/st2110-toolkit/nmos 12 | NMOS_LOGFILE=/tmp/nmos- 13 | 14 | usage() 15 | { 16 | echo "$0 is a wrapper script that manages EBU-LIST 17 | and related sub-services (DB, backend, UI, etc.) 18 | Usage: 19 | $0 {start|stop|log} 20 | start start nmos-cpp-node and node_poller.py 21 | stop stop all 22 | log 23 | " >&2 24 | } 25 | 26 | start() 27 | { 28 | $NMOS_CPP_PATH/nmos-cpp-node $NMOS_ST2110_PATH/nmos-cpp-ffmpeg-dnssd-config.json 2>&1> ${NMOS_LOGFILE}cpp.log & 29 | sleep 4 30 | #TODO grap port from nmos-ccp config 31 | $NMOS_ST2110_PATH/node_poller.py localhost:8081 > ${NMOS_LOGFILE}poller.log & 32 | } 33 | 34 | stop() 35 | { 36 | killall nmos-cpp-node 37 | killall node_poller.py 38 | } 39 | 40 | log() 41 | { 42 | tail -f ${NMOS_LOGFILE}* 43 | } 44 | 45 | case "$1" in 46 | start) 47 | start 48 | ;; 49 | stop) 50 | stop 51 | ;; 52 | log) 53 | log 54 | ;; 55 | *) 56 | usage 57 | exit 1 58 | ;; 59 | esac 60 | 61 | exit 0 62 | -------------------------------------------------------------------------------- /nmos/node_connection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import json 4 | from nmos_node import NmosNode 5 | import time 6 | 7 | def usage(): 8 | print(""" 9 | node_connection.py - this script (dis)activates a sender or a receiver 10 | node based on connection API (IS-05). 11 | 12 | Usage: 13 | \tnode_connection.py 14 | 15 | """) 16 | 17 | def connection_log(msg): 18 | print("[connection] " + msg) 19 | 20 | def main(): 21 | if len(sys.argv) < 4: 22 | usage() 23 | return 24 | 25 | sender = NmosNode(ip = sys.argv[1], type = 'tx') 26 | receiver = NmosNode(ip = sys.argv[2], type = 'rx') 27 | state = True if sys.argv[3] == 'start' else False 28 | 29 | connection_log("Disactivate all rx") 30 | #receiver.activate_all(False) 31 | if not state: 32 | return 33 | 34 | # For Embrionix EMSFP: 1st is video and 2nd is audio 35 | for tx_id in sender.get_ids()[:2]: 36 | connection_log("*" * 72) 37 | connection_log("Activate tx id:" + tx_id) 38 | sender.activate(state, tx_id) 39 | 40 | connection_log("GET tx SDP from tx id:" + tx_id) 41 | sdp = sender.get_sdp(tx_id) 42 | if 'video' in sdp: 43 | connection_log("Video detected") 44 | rx_id = receiver.get_video_id() 45 | elif 'audio' in sdp: 46 | connection_log("Audio detected") 47 | rx_id = receiver.get_audio_id() 48 | else: 49 | connection_log("unknown media in sdp:" + sdp) 50 | return 51 | 52 | connection_log("PATCH rx id:" + rx_id) 53 | receiver.set_connection_sdp(rx_id, tx_id, sdp) 54 | connection_log("Activate rx id:" + rx_id) 55 | receiver.activate(state, rx_id) 56 | 57 | #connection_log("..............") 58 | #time.sleep(2) 59 | #connection_log(receiver.get_connection_status(rx_id)) 60 | #connection_log(receiver.get_connection_sdp(rx_id)) 61 | 62 | if __name__ == "__main__": 63 | main() 64 | connection_log("Exit.") 65 | -------------------------------------------------------------------------------- /nmos/node_controller.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | import sys 3 | import json 4 | from time import sleep 5 | from nmos_node import NmosNode 6 | 7 | def usage(): 8 | print(""" 9 | node_controller.py - this script (dis)activates a sender or a receiver 10 | node based on connection API (IS-05). 11 | 12 | Usage: 13 | \tnode_controller.py 14 | 15 | """) 16 | 17 | def main(): 18 | if len(sys.argv) < 4: 19 | usage() 20 | return 21 | 22 | node = NmosNode(ip = sys.argv[1], type = sys.argv[2]) 23 | node.activate_all(True if sys.argv[3] == 'start' else False) 24 | 25 | if __name__ == "__main__": 26 | main() 27 | -------------------------------------------------------------------------------- /nmos/node_poller.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | 3 | import sys 4 | import json 5 | import hashlib 6 | import time 7 | from nmos_node import NmosNode 8 | import os.path 9 | import subprocess 10 | import re 11 | 12 | def usage(): 13 | print(""" 14 | node_poller.py - this script polls the NOMS connection API (IS-05) of a 15 | \treceiver to get active connections and start ffmpeg transcoder to join 16 | \tthe same multicast group. 17 | 18 | Usage: 19 | \tnode_poller.py 20 | 21 | """) 22 | 23 | def poller_log(msg): 24 | print("[poller] " + msg) 25 | 26 | def transcode(active, sdp_file): 27 | user=os.environ['ST2110_USER'] 28 | transcoder='/home/'+user+'/st2110-toolkit/transcode.sh' 29 | if not active: 30 | res = subprocess.check_output([transcoder, 'stop']) 31 | else: 32 | res = subprocess.check_output([transcoder, 'start', sdp_file]) 33 | poller_log(res) 34 | 35 | def poll(node): 36 | sdp_filename = "/tmp/sdp.sdp" 37 | if os.path.exists(sdp_filename): 38 | os.remove(sdp_filename) 39 | 40 | ids = node.get_ids() 41 | state = {} 42 | 43 | old_sdp_filtered="" 44 | 45 | while True: 46 | time.sleep(2) 47 | poller_log("-" * 72) 48 | # process every receiver of the node 49 | for rx_id in ids: 50 | # get connection status and sdp 51 | state[rx_id] = {} 52 | state[rx_id]['connection_status'] = node.get_connection_status(rx_id) 53 | state[rx_id]['sdp'] = node.get_connection_sdp(rx_id) 54 | state[rx_id]['media_type'] = node.get_media_type(rx_id) 55 | poller_log(rx_id +"(" + state[rx_id]['media_type'] + "): active=" + str(state[rx_id]['connection_status']) + " SDP=" + ("None" if state[rx_id]['sdp'] == None else "OK")) 56 | 57 | # combine SDPs into a single one for ffmpeg 58 | sdp_filtered="" 59 | sdp_already_has_description = False 60 | for rx_id in ids: 61 | if not state[rx_id]['connection_status'] or not state[rx_id]['sdp']: 62 | continue 63 | sdp = state[rx_id]['sdp'] 64 | 65 | # remove -7 redundant stream by keeping 'primary' part of SDP only 66 | delimiter='m=' 67 | sdp_chunks = [delimiter+e for e in sdp.split(delimiter) if e] 68 | sdp_chunks[0] = sdp_chunks[0][len(delimiter):] 69 | sdp = "".join([ i for i in sdp_chunks if 'primary' in i ]) 70 | 71 | if not sdp_already_has_description: 72 | # 1st flow: keep description but add a separator 73 | expr = re.compile(r'(^t=.*\n)', re.MULTILINE) 74 | sdp = re.sub(expr, r'\1\n', sdp) 75 | sdp_already_has_description = True 76 | else: 77 | # other flows: skip description 78 | expr = re.compile(r'^o=.*\n|^v=.*\n|s=.*\n|t=.*\n', re.MULTILINE) 79 | sdp = re.sub(expr, '', sdp) 80 | 81 | sdp_filtered += sdp 82 | 83 | # do nothing when content hasn't changed 84 | if old_sdp_filtered == sdp_filtered: 85 | continue 86 | 87 | if sdp_filtered == "": 88 | # all the receivers are disabled 89 | transcode(False, 'dummy') 90 | else: 91 | # write re-arranged sdp file 92 | poller_log("SDP:\n" + str(sdp_filtered)) 93 | file = open(sdp_filename, 'w') 94 | file.write(sdp_filtered) 95 | file.close() 96 | # restart transcoder 97 | transcode(False, 'dummy') 98 | transcode(True, sdp_filename) 99 | 100 | old_sdp_filtered = sdp_filtered 101 | 102 | #TODO: ffmpeg status? 103 | 104 | def main(): 105 | if len(sys.argv) < 2: 106 | usage() 107 | return 108 | 109 | node = NmosNode(sys.argv[1]) 110 | poll(node) 111 | 112 | if __name__ == "__main__": 113 | main() 114 | -------------------------------------------------------------------------------- /nmos/patch_sdp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ $# -lt 4 ]; then 4 | echo "Usage: $0 " 5 | exit 1 6 | fi 7 | 8 | # args 9 | ip=$1 10 | id=$2 11 | sdp=$3 12 | if [ $4 = "on" ]; then 13 | activate=true 14 | else 15 | activate=false 16 | fi 17 | 18 | # sdp to json 19 | dos2unix $sdp 20 | transport_file=$(sed ':a;N;$!ba;s/\n/\\n/g' $sdp) # replace newlines with '\\n' 21 | json_on='{"sender_id":null,"activation":{"mode":"activate_immediate","requested_time":null},"master_enable":'$activate',"transport_file":{"data":"'$transport_file'","type":"application/sdp"}}' 22 | echo $json_on > $sdp.json 23 | 24 | # sent to nmos uri 25 | uri="http://${ip}/x-nmos/connection/v1.0/single/receivers/${id}/staged" 26 | #echo "=== PATCH $json > $uri" 27 | curl \ 28 | -w "@$(dirname $0)/curl-format.txt" \ 29 | --header "Content-Type: application/json" \ 30 | -X PATCH \ 31 | --data @"$sdp.json" \ 32 | $uri 33 | -------------------------------------------------------------------------------- /pcap/ancillary_editor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Author "Patrick Keroulas" 4 | # 5 | # Role: 6 | # - take a pcap file as argument 7 | # - interpret packet as st2110-40 (RTP, ancillary data) 8 | # - change anc header or paylaod depending on params below: 9 | # * field bits to 0 10 | # * the DID/SDID of Anciallary Time Code to 0x01 (unacceptable) 11 | # - recalculate the checksum 12 | # - write the ouput as a file 13 | # - !!! works only for one Anc payload per packets !!! 14 | # - !!! works only if modified packets is same length as original !!! 15 | 16 | # Instead of passing arg, activate these params: 17 | EDIT_ENABLED = True 18 | EDIT_MARKER_ENABLED = False 19 | EDIT_FIELD_ENABLED = False 20 | EDIT_DID_ENABLED = True 21 | EDIT_PAYLOAD_ENABLED = True 22 | EDIT_ALL = False 23 | EDIT_PKT_IDS = [10, 55] # if not EDIT_ALL; IDs: like displayed in wireshark 24 | 25 | import sys 26 | from array import array 27 | import io 28 | from scapy.all import * 29 | 30 | if (len(sys.argv) < 2): 31 | print(sys.argv[0] + ' ') 32 | exit(-1) 33 | 34 | CRC_MASK = 0x01ff 35 | 36 | class BitWriter(object): 37 | def __init__(self, f): 38 | self.accumulator = 0 39 | self.bcount = 0 40 | self.out = f 41 | 42 | def __enter__(self): 43 | return self 44 | 45 | def __exit__(self, exc_type, exc_val, exc_tb): 46 | self.flush() 47 | 48 | def __del__(self): 49 | try: 50 | self.flush() 51 | except ValueError: # I/O operation on closed file. 52 | pass 53 | 54 | def _writebit(self, bit): 55 | if bit > 0: 56 | self.accumulator |= 1 << 7-self.bcount 57 | self.bcount += 1 58 | 59 | def writebits(self, bits, n): 60 | while n > 0: 61 | if self.bcount == 8: 62 | self.flush() 63 | self._writebit(bits & 1 << n-1) 64 | n -= 1 65 | if self.bcount == 8: 66 | self.flush() 67 | 68 | def flush(self): 69 | self.out.write(bytearray([self.accumulator])) 70 | self.accumulator = 0 71 | self.bcount = 0 72 | 73 | 74 | class BitReader(object): 75 | def __init__(self, f): 76 | self.input = f 77 | self.accumulator = 0 78 | self.bcount = 0 79 | self.read = 0 80 | 81 | def __enter__(self): 82 | return self 83 | 84 | def __exit__(self, exc_type, exc_val, exc_tb): 85 | pass 86 | 87 | def _readbit(self): 88 | if not self.bcount: 89 | a = self.input.read(1) 90 | if a: 91 | self.accumulator = ord(a) 92 | self.bcount = 8 93 | self.read = len(a) 94 | rv = (self.accumulator & (1 << self.bcount-1)) >> self.bcount-1 95 | self.bcount -= 1 96 | return rv 97 | 98 | def readbits(self, n): 99 | v = 0 100 | while n > 0: 101 | v = (v << 1) | self._readbit() 102 | n -= 1 103 | return v 104 | 105 | def get_parity(value): 106 | p = 0 107 | for i in range(8): 108 | if value & 1: 109 | p += 1 110 | value >= 1 111 | print("parity: "+ str(p)) 112 | return int(p & 1) 113 | 114 | def editPayload(reader, writer): 115 | edited = 0 116 | tab=[] 117 | while True: 118 | tab.append(reader.readbits(10)) 119 | if not reader.read: # nothing to read 120 | break 121 | 122 | old_crc = 0 # only for debug 123 | new_crc = 0 124 | data_count = 0 125 | for i, t in enumerate(tab): 126 | msg="" 127 | 128 | if i == 0: 129 | msg="DID = " + hex(t & 0xff) 130 | old_crc += t 131 | if (((t & 0xff) == 0x60) and EDIT_DID_ENABLED): 132 | print(" ... Ancillary Time Code edited 0x60 -> 0x01") 133 | edited = 1 134 | tab[i] = 1 135 | new_crc += tab[i] 136 | 137 | elif i == 1: 138 | msg="SDID = " + hex(t & 0xff) 139 | old_crc += t 140 | if edited: 141 | print(" ... edited 0xxx -> 0x101") 142 | tab[i] = 0x101 143 | new_crc += tab[i] 144 | 145 | elif i == 2: 146 | data_count = t & 0xff 147 | old_crc += t 148 | new_crc += t 149 | #msg="data_count = " + str(data_count) 150 | 151 | elif i == 3 and EDIT_PAYLOAD_ENABLED: 152 | data_count = t & 0xff 153 | old_crc += t 154 | new_crc += t 155 | tab[i] = ~t; 156 | new_crc += tab[i] 157 | msg="payload[0]: " + str(t) +" -> " + str(tab[i]) 158 | 159 | elif i == data_count + 3: # this is checksum 160 | if edited: 161 | # compute new crc 162 | tab[i] = new_crc & CRC_MASK 163 | if not new_crc & 0x100: 164 | new_crc = new_crc + 0x200 165 | 166 | msg="sum = " + hex(t & CRC_MASK) + ", crc = " + hex(old_crc & CRC_MASK) + ", new crc = " + hex(tab[i]) 167 | 168 | elif i > data_count + 3: #this might not work for other paylaod 169 | msg="stuffing" 170 | 171 | else: 172 | old_crc += t 173 | new_crc += t 174 | msg="data = "+ str((t&0b1111111111) >> 2) 175 | 176 | if not msg == "": 177 | print("raw:" + hex(t)+ "->" + str(int(t)) + " " + msg) 178 | 179 | writer.writebits(tab[i],10) 180 | 181 | """ 182 | https://tools.ietf.org/id/draft-ietf-payload-rtp-ancillary-14.txt 183 | 184 | 0 1 2 3 185 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 186 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 187 | |V=2|P|X| CC |M| PT | sequence number | 188 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 189 | | timestamp | 190 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 191 | | synchronization source (SSRC) identifier | 192 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 193 | | Extended Sequence Number | Length=32 | 194 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 195 | | ANC_Count=2 | F | reserved | 196 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 197 | |C| Line_Number=9 | Horizontal_Offset |S| StreamNum=0 | 198 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 199 | | DID | SDID | Data_Count=0x84 | 200 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 201 | ............ 202 | """ 203 | 204 | # open capture file and iterate on pkts 205 | cap = rdpcap(sys.argv[1]) 206 | for index, pkt in enumerate(cap): 207 | # init streams udp payload: RTP 208 | i_stream = io.BytesIO(pkt.load) 209 | o_stream = io.BytesIO(pkt.load) 210 | if not EDIT_ENABLED or not (index+1 in EDIT_PKT_IDS or EDIT_ALL): 211 | continue 212 | 213 | # init bit readers 214 | reader = BitReader(i_stream) 215 | writer = BitWriter(o_stream) 216 | 217 | print("=====================================") 218 | buf = i_stream.getvalue() 219 | print("in [" + str(index) +"], l" + str(len(buf)) + ":" + str([hex(i) for i in buf])) 220 | 221 | if EDIT_MARKER_ENABLED: 222 | # jump to 'M' field 223 | i_stream.seek(1) 224 | o_stream.seek(1) 225 | bit = reader.readbits(8) 226 | writer.writebits(bit^0x80, 8) 227 | print("RTP Marker bit: " + hex(bit) + " --> " + hex(bit^0x80)) 228 | 229 | if EDIT_FIELD_ENABLED: 230 | # jump to 'F' field 231 | i_stream.seek(17) 232 | o_stream.seek(17) 233 | field = reader.readbits(8) 234 | writer.writebits(field&0b00111111, 8) 235 | print("Field: " + hex(field >> 6) + " --> 0") 236 | 237 | if EDIT_DID_ENABLED or EDIT_PAYLOAD_ENABLED: 238 | # jump to DID/SDID 239 | i_stream.seek(24) 240 | o_stream.seek(24) 241 | editPayload(reader, writer) 242 | 243 | buf = o_stream.getvalue() 244 | print("out [" + str(index) +"], l" + str(len(buf)) + ":" + str([hex(i) for i in buf])) 245 | pkt.load = o_stream.getvalue() 246 | 247 | # write output file 248 | print("Output file: /tmp/out.pcap") 249 | wrpcap('/tmp/out.pcap', cap) 250 | -------------------------------------------------------------------------------- /pcap/audio_raw_extractor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Author "Patrick Keroulas" 4 | # 5 | # Extracts AES 67 payload from pcap and suggest ffmpeg cmd to convert to 6 | # wav. Note that you need to give audio params (from the SDP) for the 7 | # conversion. 8 | 9 | import sys 10 | from scapy.all import * 11 | import io 12 | import shutil 13 | 14 | if len(sys.argv) < 3: 15 | print(sys.argv[0] + ' \n\ 16 | \n\ 17 | output: output.raw \n\ 18 | \n\ 19 | Exple: \n\ 20 | $ ' + sys.argv[0] + ' st2110-30-capture.pcap 225.0.0.1') 21 | exit(-1) 22 | 23 | pcap = sys.argv[1] 24 | filter = 'dst ' + sys.argv[2] 25 | raw_filename = 'output.raw' 26 | raw_file = open(raw_filename, mode='wb') 27 | pkt_counter = 0 28 | 29 | # In the RTP header: 30 | # Skip Flags and Payload type (2), Seq num(2), Timestamp(4), SSRC(4), 31 | # Assume there is no Ext Seq Num(2) 32 | OFFSET=12 33 | 34 | def showProgess(progress): 35 | sys.stdout.write("%s \r" % (progress) ) 36 | sys.stdout.flush() 37 | 38 | def extractPayload(pkt): 39 | global pkt_counter 40 | global raw_file 41 | 42 | raw_file.write(pkt.load[OFFSET:]) 43 | 44 | showProgess("pkt="+str(pkt_counter)) 45 | pkt_counter += 1 46 | 47 | print('Filter dst IP: \'' + filter + '\'') 48 | print('Processing...') 49 | sniff(offline=pcap, filter=filter, store = 0, prn = extractPayload) 50 | 51 | raw_file.close() 52 | 53 | print('Done') 54 | print('Output: ' + raw_filename ) 55 | print('Processed packets: ' + str(pkt_counter)) 56 | print("Suggestions:\n\ 57 | - convert to wav, using audio params from SDP file:\n\ 58 | exple: 24-bit, 2 ch, sample rate: 48k 59 | $ ffmpeg -hide_banner -y -f s24be -ar 48k -ac 2 -i output.raw output.wav\n\ 60 | - then, playback\n\ 61 | $ vlc output.wav"\ 62 | ) 63 | -------------------------------------------------------------------------------- /pcap/pkt_drop_detector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Author "Patrick Keroulas" 4 | # 5 | # Count packets and drops for every RTP stream found in a given pcap. 6 | # The method is based on `Sequence Number` jump detection, assuming no 7 | # packet reordering. 8 | 9 | import sys 10 | from array import array 11 | import pprint 12 | import io 13 | from scapy.all import * 14 | 15 | if (len(sys.argv) < 2): 16 | print(sys.argv[0] + ' ') 17 | exit(-1) 18 | 19 | pcap = sys.argv[1] 20 | filter ='' 21 | 22 | def showProgess(progress): 23 | sys.stdout.write("packets: %d \r" % (progress) ) 24 | sys.stdout.flush() 25 | 26 | """ 27 | RTP header: 28 | 29 | 0 1 2 3 30 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 31 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 32 | |V=2|P|X| CC |M| PT | sequence number | 33 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 34 | | timestamp | 35 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 36 | | synchronization source (SSRC) identifier | 37 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 38 | | Extended Sequence Number | Length=32 | 39 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 40 | ............ 41 | """ 42 | 43 | counters = {} 44 | index = 0 45 | def checkSeqNum(pkt): 46 | global counters, index 47 | showProgess(index) 48 | index +=1 49 | 50 | # notes: 51 | #print(pkt.getlayer(IP).dst) 52 | #print(pkt[TCP].sport) 53 | 54 | # init streams udp payload: RTP 55 | try: 56 | buf = io.BytesIO(pkt.load).getvalue() 57 | except Exception as e: 58 | print(e) 59 | print(pkt) 60 | return 61 | 62 | # read RTP sequence number 63 | #ts = (ord(buf[2]) << 8 ) + ord(buf[3]) 64 | ts = (buf[2] << 8 ) + buf[3] 65 | 66 | desc = pkt.summary() 67 | if desc not in counters.keys(): 68 | print("\nNew: " + desc) 69 | counters[desc] = {'pkt': 1, 'drop': 0 , 'old_ts': ts } 70 | return 71 | counters[desc]['pkt'] += 1; 72 | 73 | # include UDP but not PTP (which is not RTP) 74 | if 'ptp_' in desc or not 'UDP' in desc: 75 | return 76 | 77 | old_ts = counters[desc]['old_ts'] 78 | if (old_ts != None) and ((old_ts + 1) != ts) and not ((old_ts == 65535) and (ts == 0)): 79 | drop = (ts - old_ts) - 1 80 | print(desc + ' drop('+ str(old_ts) + '...' + str(ts) + ') = ' + str(drop) + ' pkts!') 81 | counters[desc]['drop'] += drop; 82 | 83 | counters[desc]['old_ts'] = ts 84 | 85 | 86 | # GO! 87 | print('Filter dst IP: \'' + filter + '\'') 88 | print('Processing...') 89 | 90 | sniff(offline=pcap, filter=filter, store = 0, prn = checkSeqNum) 91 | print('Done. ') 92 | print("Pkts counters:") 93 | pprint.pprint(counters) 94 | -------------------------------------------------------------------------------- /pcap/video_yuv_extractor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Author "Patrick Keroulas" 4 | # 5 | # Extract RFC 4175 payload of ST 2110-20 packets given dst mcast IP 6 | # and write 'output.yuv' 7 | # 8 | # At CBC, a very common pix format in RFC4175 is: packed YUV 4:2:2 9 | # 10-bit/component but this is not supported by ffmpeg. So several pix 10 | # format alternatives are prosposed: 11 | # packed 4:2:2 8-bit > FFmpeg: "uyvy42u" 12 | # planar 4:2:2 8-bit > FFmpeg: "yuv422p" 13 | # planar 4:2:2 10-bit > FFmpeg: "yuv422p10be" 14 | # 15 | # More info about YUV pixel formats: 16 | # - http://www.fourcc.org/yuv.php#UYVY 17 | # - FFmpeg/libavutil/pixfmt.h 18 | # - $ ffmpeg -pix_fmts 19 | # 20 | # playback: 21 | # $ ffplay -f rawvideo -vcodec rawvideo -s 1920*540 -pix_fmt uyvy422 -i output.yuv 22 | 23 | import sys 24 | import io 25 | from scapy.all import * 26 | import shutil 27 | from collections import namedtuple 28 | from bitstruct import * 29 | 30 | if (len(sys.argv) < 4): 31 | print(sys.argv[0] + ' \n\ 32 | yuv_mode: 1 = packed 4:2:2 8-bit > FFmpeg: "uyvy42u"\n\ 33 | 2 = planar 4:2:2 8-bit > FFmpeg: "yuv422p"\n\ 34 | 3 = planar 4:2:2 10-bit > FFmpeg: "yuv422p10be"\n\ 35 | \n\ 36 | output: output.yuv \n\ 37 | \n\ 38 | Exple: \n\ 39 | $ ' + sys.argv[0] + ' st2110-20-capture.pcap 225.192.1.14 2') 40 | exit(-1) 41 | 42 | pcap = sys.argv[1] 43 | filter = 'dst ' + sys.argv[2] 44 | YUV_MODE = ['uyvy422', 'yuv422p', 'yuv422p10be'] 45 | yuv_mode = YUV_MODE[int(sys.argv[3])-1] 46 | yuv_filename = 'output.yuv' 47 | yuv_file = open(yuv_filename, mode='wb') 48 | 49 | y_stream = io.BytesIO(bytes()) 50 | u_stream = io.BytesIO(bytes()) 51 | v_stream = io.BytesIO(bytes()) 52 | 53 | def showProgess(progress): 54 | sys.stdout.write("%s \r" % (progress) ) 55 | sys.stdout.flush() 56 | 57 | """ 58 | RFC 4175 datagram: 59 | 60 | 0 1 2 3 61 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 62 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 63 | | V |P|X| CC |M| PT | Sequence Number | 64 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 65 | | Time Stamp | 66 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 67 | | SSRC | 68 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 69 | | Extended Sequence Number | Length | 70 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 71 | |F| Line No |C| Offset | 72 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 73 | | Length |F| Line No | 74 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 75 | |C| Offset | . 76 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ . 77 | . . 78 | . Two (partial) lines of video data . 79 | . . 80 | +---------------------------------------------------------------+ 81 | """ 82 | 83 | # line header defined by RFC 4175 84 | LineHeader = namedtuple('line_header', ['length', 'field', 'line', 'continuation', 'offset']) 85 | line_header_compiler = compile('u16u1u15u1u15') 86 | line_header_size = 6 87 | l_max = 0 88 | o_max = 0 89 | 90 | def getLineHeader(f): 91 | global LineHeader, line_header_compiler, line_header_size, frame_counter 92 | global l_max, o_max 93 | 94 | unpacked = line_header_compiler.unpack(f.read(line_header_size)) 95 | header = LineHeader(*unpacked) 96 | 97 | # compute WxH for the 1st frame only 98 | if frame_counter == 0: 99 | if header.line > l_max: 100 | l_max = header.line 101 | if header.offset + (header.length / 5 * 2) > o_max: # a pgroup is 5-byte for 2 px in 4:2:2 10-bit 102 | o_max = header.offset + int(header.length / 5 * 2) 103 | 104 | #showProgess("frame="+str(frame_counter)+", line="+str(header.line) + ", offset= " + str(header.offset) + "c=" + str(header.continuation)) 105 | showProgess("frame="+str(frame_counter)+", line="+str(header.line)) 106 | 107 | return header 108 | 109 | # pixel group: packed 4:2:2 10-bit 110 | PGroup = namedtuple('pgroup', ['u', 'y0', 'v', 'y1']) 111 | pgroup_compiler = compile('u10u10u10u10') 112 | pgroup_size = 5 113 | 114 | def getLinePayload(f, h): 115 | global PGroup, pgroup_compiler, pgroup_size 116 | global yuv_mode, yuv_file, y_stream, u_stream, v_stream 117 | 118 | i = 0 119 | while i < h.length: 120 | i += pgroup_size 121 | unpacked = pgroup_compiler.unpack(f.read(pgroup_size)) 122 | p = PGroup(*unpacked) 123 | 124 | if (yuv_mode == 'uyvy422'): 125 | #y_stream.write(pack('u8u8u8u8', p.u>>2, p.y0>>2, p.v>>2, p.y1>>2)) # less performant 126 | y_stream.write(bytes([p.u>>2, p.y0>>2, p.v>>2, p.y1>>2])) 127 | 128 | elif (yuv_mode == 'yuv422p'): 129 | y_stream.write(bytes([p.y0>>2, p.y1>>2])) 130 | u_stream.write(bytes([p.u>>2])) 131 | v_stream.write(bytes([p.v>>2])) 132 | 133 | elif (yuv_mode == 'yuv422p10be'): 134 | # TODO see if bytes()... is faster 135 | y_stream.write(pack('u16u16', p.y0, p.y1)) 136 | u_stream.write(pack('u16', p.u)) 137 | v_stream.write(pack('u16', p.v)) 138 | 139 | # extrator state 140 | recording = False 141 | frame_counter = 0 142 | 143 | def extractPayload(pkt): 144 | global recording, frame_counter 145 | global yuv_mode, yuv_file, y_stream, u_stream, v_stream 146 | 147 | i_stream = io.BytesIO(pkt.load) 148 | 149 | # go find RTP marker bit 150 | i_stream.seek(1) 151 | marker = (i_stream.getvalue()[1] >> 7) & 0x01; 152 | if marker == 1: 153 | # 1st end of frame 154 | if not recording: 155 | recording = True 156 | return 157 | if not recording: 158 | # skip 1st uncomplete frame 159 | return 160 | 161 | # skip Flags and Payload type (2), Seq num(2), Timestamp(4), SSRC(4), Ext Seq Num(2) 162 | i_stream.seek(14) 163 | 164 | h0 = getLineHeader(i_stream) 165 | if (h0.continuation): 166 | h1 = getLineHeader(i_stream) 167 | 168 | getLinePayload(i_stream, h0) 169 | if (h0.continuation): 170 | getLinePayload(i_stream, h1) 171 | # have never seen more than 2 lines/pkt 172 | 173 | # frame complete 174 | if marker == 1: 175 | y_stream.seek(0) 176 | shutil.copyfileobj(y_stream, yuv_file) 177 | y_stream.seek(0) 178 | 179 | if (not yuv_mode == 'uyvy422'): 180 | u_stream.seek(0) 181 | v_stream.seek(0) 182 | shutil.copyfileobj(u_stream, yuv_file) 183 | shutil.copyfileobj(v_stream, yuv_file) 184 | u_stream.seek(0) 185 | v_stream.seek(0) 186 | 187 | frame_counter += 1 188 | 189 | # GO! 190 | print('Filter dst IP: \'' + filter + '\'') 191 | print('Processing...') 192 | 193 | sniff(offline=pcap, filter=filter, store = 0, prn = extractPayload) 194 | yuv_file.close() 195 | 196 | print('Done. ') 197 | print("Suggestions:\n\ 198 | - playback:\n\ 199 | ffplay -f rawvideo -vcodec rawvideo -s " +str(o_max)+ "x" + str(l_max+1) + " -pix_fmt " + yuv_mode + " -i " + yuv_filename + "\n\ 200 | - convert to .mov:\n\ 201 | ffmpeg -f rawvideo -vcodec rawvideo -r 59.94 -s " +str(o_max)+ "x" + str(l_max+1) + " -pix_fmt " + yuv_mode + " -i " + yuv_filename + " -vf 'tinterlace=merge' -c:v v210 -pix_fmt yuv422p10le output.mov \n\ 202 | ") 203 | 204 | -------------------------------------------------------------------------------- /ptp/0001-port-do-not-answer-in-case-of-unknown-mgt-message-co.patch: -------------------------------------------------------------------------------- 1 | diff --git a/port.c b/port.c 2 | index fa49663..73c2ca1 100644 3 | --- a/port.c 4 | +++ b/port.c 5 | @@ -2869,7 +2869,6 @@ int port_manage(struct port *p, struct port *ingress, struct ptp_message *msg) 6 | port_management_send_error(p, ingress, msg, TLV_NOT_SUPPORTED); 7 | break; 8 | default: 9 | - port_management_send_error(p, ingress, msg, TLV_NO_SUCH_ID); 10 | return -1; 11 | } 12 | return 1; 13 | -------------------------------------------------------------------------------- /ptp/README.md: -------------------------------------------------------------------------------- 1 | # linuxptp utilities 2 | 3 | A good synchronization is mandatory to make a capture device precise. 4 | Mellanox NIC capabilities include hardware timestamping of Tx and Rx 5 | packets to allow both a reliable synchronization to PTP grand master 6 | and accurate timestamp in captures. The following doc relies on 7 | `linuxptp` which includes 3 utilities: 8 | 9 | ## Setup 10 | 11 | PTP client for tight sync and accurate packet timestamping. Install 12 | still as root user form top directory: 13 | 14 | ```sh 15 | ./install.sh ptp 16 | ``` 17 | 18 | Config file is `/etc/ptp/ptp4l.conf`. Control the service with systemctl: 19 | 20 | ``` 21 | sudo systemctl status|start|stop ptp 22 | ``` 23 | 24 | ## ptp4l 25 | 26 | `ptp4l` handle PTP traffic to synchronizes the local NIC clock to a 27 | remote master clock: 28 | 29 | ``` 30 | $ ptp4l -f /etc/linuxptp/ptp4l.conf -s -i 31 | $ journalctl -f | grep 'ptp4\|phc2' 32 | [...] ptp4l: [601999.078] rms 20 max 32 freq -3106 +/- 28 delay 159 +/- 2 33 | [...] ptp4l: [602000.090] rms 18 max 28 freq -3107 +/- 24 delay 157 +/- 6 34 | ``` 35 | 36 | Note that all the time values are in nanosec. 37 | 38 | ## phc2sys 39 | 40 | Then `phc2sys` controls system/OS clock to be synced with NIC clock. 41 | 42 | ``` 43 | $ journalctl -f | grep 'ptp4\|phc2' 44 | $ phc2sys -s -c CLOCK_REALTIME -w -n 45 | [...] phc2sys: [601999.702] phc offset 25 s2 freq -1077 delay 1094 46 | [...] phc2sys: [601998.702] phc offset -9 s2 freq -1108 delay 1100 47 | ``` 48 | 49 | ## pmc 50 | 51 | Show PTP sync status and metrics: 52 | 53 | ``` 54 | sudo pmc -d -u -b 2 'GET CURRENT_DATA_SET' 55 | sudo pmc -d -u -b 2 'GET PARENT_DATA_SET' 56 | ``` 57 | 58 | Root priviledge is under [discussion](https://www.mail-archive.com/linuxptp-devel@lists.sourceforge.net/msg05540.html) 59 | for 3.1.2. Tested on commit @4d9f44. It works proveded `uds_file_mode 0666` 60 | and a non-default interface (`-i`): 61 | 62 | ``` 63 | pmc -i /tmp/pmc -d 88 -u -b 2 'GET CURRENT_DATA_SET' 64 | ``` 65 | 66 | ## Hardware Clock 67 | 68 | It is important to verify that the HW clock on the NIC is the actual 69 | source of timestamp for `tcpdump`/`libcap`. Verify that the dev file is 70 | usable. 71 | 72 | ```sh 73 | $ sudo hwstamp_ctl -i ens224 -r 1 74 | current settings: 75 | tx_type 0 76 | rx_filter 1 77 | SIOCSHWTSTAMP failed: Resource temporarily unavailable # RED FLAG with a Intel in a VM !!!!!!! 78 | $ lsmod | grep pps 79 | pps_core 20480 1 ptp 80 | ``` 81 | 82 | `testptp` can interact with this clock to manually set and measure both hardware and system time. 83 | 84 | ```sh 85 | $ uname -a # get your kernel version 86 | Linux ..... 4.15.0-112-generic .... 87 | $ wget https://raw.githubusercontent.com/torvalds/linux/v4.15/tools/testing/selftests/ptp/testptp.c # get ptp tester from kernel source 88 | $ gcc -o testptp testptp.c -lrt # compile it using `librt` 89 | # make sure that `ptp4l` and `phc2sys` are off 90 | $ sudo ethtool -T enp101s0f1 | grep PTP # find the proper ptp device id 91 | PTP Hardware Clock: 3 92 | $ ./testptp -d /dev/ptp3 -T 3333333 # in sec 93 | set time okay 94 | $ ./testptp -d /dev/ptp3 -g 95 | clock time: 3333336.759303339 or Sun Feb 8 08:55:36 1970 96 | $ ./testptp -d /dev/ptp3 -k 1 97 | system and phc clock time offset request okay 98 | system time: 1589581164.437482989 99 | phc time: 1589581201.437483653 100 | system time: 1589581164.437484167 101 | system/phc clock time offset is -37000000075 ns # 37s is the difference between UTC and International Atomic Time (TAI) 102 | system clock time delay is 1178 ns 103 | ``` 104 | 105 | Then `tcpdump -j adapter_unsynced ...` will provide capture from 1970 106 | regardless of the local system time. Turn on `ptp4l` to restore PTP 107 | current time. 108 | 109 | ## linuxptp_sync_graph.py 110 | 111 | This tool measures the precision of a system clock regarding of a grand 112 | master. It is supposed to run on a workstation and remotely executes 113 | `pmc` (Ptp Management Client) to provide time offset of remote nodes and 114 | plot on a graph. 115 | 116 | ## TODOs 117 | 118 | In case of high traffic, Mellanox NIC can steer PTP in a dedicated 119 | buffer: https://community.mellanox.com/s/article/howto-steer-ptp-traffic-to-single-rx-ring--via-ethtool-x 120 | 121 | Refine linuxptp_sync_graph and make it more convenient. 122 | -------------------------------------------------------------------------------- /ptp/install.sh: -------------------------------------------------------------------------------- 1 | export PTP_VERSION=3.1 2 | 3 | install_ptp() 4 | { 5 | # build+patch linuxptp instead of installing pre-built package 6 | cd $TOP_DIR 7 | dir=$(pwd) 8 | echo "Installing PTP" 9 | DIR=$(mktemp -d) 10 | cd $DIR/ 11 | git clone http://git.code.sf.net/p/linuxptp/code linuxptp 12 | cd linuxptp 13 | git checkout -b $PTP_VERSION v$PTP_VERSION 14 | # https://sourceforge.net/p/linuxptp/mailman/linuxptp-devel/thread/014101d3ddea%24c3a76690%244af633b0%24%40de/#msg36304311 15 | patch -p1 < $dir/ptp/0001-port-do-not-answer-in-case-of-unknown-mgt-message-co.patch 16 | make 17 | make install 18 | make distclean 19 | rm -rf $DIR 20 | 21 | mkdir /etc/linuxptp 22 | install -m 644 $TOP_DIR/ptp/ptp4l.conf /etc/linuxptp/ptp4l.conf 23 | 24 | install -m 755 $TOP_DIR/ptp/ptp.init /etc/init.d/ptp 25 | update-rc.d ptp defaults 26 | systemctl enable ptp 27 | 28 | systemctl stop systemd-timesyncd.service 29 | systemctl disable systemd-timesyncd.service 30 | } 31 | -------------------------------------------------------------------------------- /ptp/linuxptp_sync_graph.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Linuxptp sync tester for multiple PTP slaves. 5 | 6 | Executed on any host running 'ptp4l', i.e. master or slave, this tool 7 | uses 'Ptp management client' to sample the offset from clock master 8 | for every slave. The outputs are a realtime-plotting graph and 9 | standard derivation. 10 | 11 | The output of pmc command looks like: 12 | sending: GET CURRENT_DATA_SET 13 | b49691.fffe.0a717c-0 seq 0 RESPONSE MANAGEMENT CURRENT_DATA_SET 14 | stepsRemoved 1 15 | offsetFromMaster 22713.0 16 | meanPathDelay 86597.0 17 | 3c970e.fffe.a94296-1 seq ... 18 | """ 19 | 20 | import re 21 | import matplotlib.pyplot as plt 22 | import numpy as np 23 | from common import Server, Command 24 | 25 | """ 26 | Params 27 | """ 28 | N = 200 # samples number (~= duration in sec) 29 | SLAVE_1_NAME = '' 30 | SLAVE_1_MAC = '' 31 | # remote host 32 | SERVER_USER = '' 33 | SERVER_PWD = '' 34 | SERVER_IP = '' 35 | PTP_DOMAIN = '' 36 | 37 | """ 38 | Clock object: 39 | It contains mac, samples and plot params 40 | """ 41 | class PtpClock: 42 | def __init__(self, name, mac, color): 43 | self.name = name 44 | self.mac = mac 45 | self.color = color 46 | self.offset = 0 47 | self.offset_buffer = [] 48 | self.mean_path_delay = 0 49 | self.mean_path_delay_buffer = [] 50 | 51 | def put_values(self, offset, mean_path_delay): 52 | self.offset = offset 53 | self.mean_path_delay = mean_path_delay 54 | 55 | def set_default_values(self): 56 | # repeat the latest sample 57 | self.offset = self.offset_buffer[-1] if self.offset_buffer else 0 58 | self.mean_path_delay = self.mean_path_delay_buffer[-1] if self.mean_path_delay_buffer else 0 59 | 60 | def update_buffers(self): 61 | self.offset_buffer.append(self.offset) 62 | self.mean_path_delay_buffer.append(self.mean_path_delay) 63 | offset_sdt = np.std(np.array(self.offset_buffer)) 64 | print("{} offset_sdt:{}".format(self.name, offset_sdt)) 65 | 66 | """ 67 | Remote execution of the Ptp Management Client 68 | """ 69 | def get_ptp_stat(server): 70 | measurement_cmd = "pmc -d %s -u -b 2 'GET CURRENT_DATA_SET'" % (PTP_DOMAIN) 71 | command = Command(server=server, wait=True, command=measurement_cmd, control=None, timeout=-1, enable=True) 72 | try: 73 | output = command.execute(server.ssh) 74 | # convert multiline text to list and remove useless command header 75 | measurement_list = output.replace("\t", "").split("\n")[2:] 76 | return [i for i in measurement_list if i != ""] 77 | 78 | except Exception as e: 79 | print(e) 80 | return [] 81 | 82 | def main(): 83 | # clock instances 84 | ptp_clocks = [] 85 | ptp_clocks.append(PtpClock(SLAVE_1_NAME, SLAVE_1_MAC, 'b')) 86 | 87 | # connect to a remote host where ptp4l is running, slave or master to 88 | # reach the ptp management channel 89 | server = Server(1, SERVER_USER, SERVER_PWD, SERVER_IP) 90 | server.connect() 91 | 92 | # interactive plot mode with labeled axis and legend 93 | fig, offset_graph = plt.subplots() 94 | path_delay_graph = offset_graph.twinx() 95 | 96 | for clk in ptp_clocks: 97 | offset_graph.plot(range(len(clk.offset_buffer)), 98 | clk.offset_buffer, 99 | "".join([clk.color, "-"]), 100 | label=" ".join([clk.name, "offset"])) 101 | path_delay_graph.plot(range(len(clk.mean_path_delay_buffer)), 102 | clk.mean_path_delay_buffer, 103 | "".join([clk.color, "."]), 104 | label=" ".join([clk.name, "path delay"])) 105 | offset_graph.legend(loc='upper left', shadow=True) 106 | path_delay_graph.legend(loc='upper right', shadow=True) 107 | 108 | offset_graph.set_xlabel("samples") 109 | offset_graph.set_ylabel("master offset (ns)") 110 | path_delay_graph.set_ylabel("mean path delay (ns)") 111 | 112 | for i in range(N): 113 | measurement_list = get_ptp_stat(server) 114 | 115 | for clk in ptp_clocks: 116 | clk.set_default_values() 117 | 118 | # parse incoming data from pmc 119 | iterator = iter(measurement_list) 120 | for c, n, o, p in zip(iterator, iterator, iterator, iterator): 121 | mac = c.split('-')[0] 122 | offset = re.sub('offsetFromMaster +', '', o) 123 | path_delay = re.sub('meanPathDelay +', '', p) 124 | 125 | if mac == clk.mac: 126 | clk.put_values(float(offset), float(path_delay)) 127 | 128 | # append buffers and plot 129 | clk.update_buffers() 130 | offset_graph.plot(range(len(clk.offset_buffer)), 131 | clk.offset_buffer, 132 | clk.color+"-", 133 | label=clk.name+" offset") 134 | offset_graph.autoscale(True, 'both', True) 135 | path_delay_graph.plot(range(len(clk.mean_path_delay_buffer)), 136 | clk.mean_path_delay_buffer, 137 | clk.color+".", 138 | label=clk.name) 139 | 140 | plt.pause(1) 141 | 142 | plt.show() 143 | 144 | if __name__ == '__main__': 145 | main() 146 | -------------------------------------------------------------------------------- /ptp/ptp.init: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | ### BEGIN INIT INFO 3 | # Provides: ptp 4 | # Required-Start: $st2110 5 | # Required-Stop: 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 8 | # Short-Description: Start linuxptp 9 | ### END INIT INFO 10 | # This header allows systemd to create a service. 11 | 12 | # To enable the initscript on SYSV init system: 13 | # Copy to /etc/init.d/ptp with root ownership 14 | # $ update-rc.d ptp defaults 15 | # $ systemctl enable ptp 16 | # $ systemctl start ptp 17 | 18 | log_ptp() 19 | { 20 | logger -t ptp "$@" 21 | } 22 | 23 | ST2110_CONF_FILE=/etc/st2110.conf 24 | if [ -f $ST2110_CONF_FILE ]; then 25 | . $ST2110_CONF_FILE 26 | fi 27 | 28 | PTP_CONFIG=/etc/linuxptp/ptp4l.conf 29 | PTP_DOMAIN=$(sed -n 's/^domainNumber\t\+\([0-9]*\)/\1/p' $PTP_CONFIG) 30 | PTP_PTP4L_PID=/var/run/ptp4l.pid 31 | PTP_PHC2SYS_PID=/var/run/phc2sys.pid 32 | 33 | start_ptp_sync() 34 | { 35 | if [ -z $1 -o -z $2 ]; then return; fi 36 | master=$1 37 | slave=$2 38 | 39 | start-stop-daemon --start --background -m --oknodo --pidfile $PTP_PHC2SYS_PID.$slave --exec /usr/local/sbin/phc2sys -- -u 16 -s $master -c $slave -w -n $PTP_DOMAIN 40 | } 41 | 42 | start_ptp() 43 | { 44 | if [ -z $PTP_IFACE_0 ]; then 45 | log_ptp "Start linuxptp: fail, no interface" 46 | return; 47 | fi 48 | 49 | log_ptp "Start linuxptp" 50 | start-stop-daemon --start --background -m --oknodo --pidfile $PTP_PTP4L_PID --exec /usr/local/sbin/ptp4l -- -f $PTP_CONFIG -s -i $PTP_IFACE_0 51 | 52 | # Sync sys clock with NIC clock 53 | start_ptp_sync $PTP_IFACE_0 'CLOCK_REALTIME' 54 | 55 | # Manage 2 ports with 2 phc2sys (1st iface is master for both sys 56 | # clock and 2nd iface). But is it better to have 2 instances of 57 | # ptp4l instead? phc2sys would dynamically choose between the 2 ifaces 58 | start_ptp_sync $PTP_IFACE_0 $PTP_IFACE_1 59 | } 60 | 61 | stop_ptp_sync() 62 | { 63 | if [ -z $1 ]; then return; fi 64 | slave=$1 65 | 66 | start-stop-daemon --stop --pidfile $PTP_PHC2SYS_PID.$slave --oknodo 67 | } 68 | 69 | stop_ptp() 70 | { 71 | log_ptp "Stop linuxptp" 72 | start-stop-daemon --stop --pidfile $PTP_PTP4L_PID --oknodo 73 | 74 | stop_ptp_sync 'CLOCK_REALTIME' 75 | stop_ptp_sync $PTP_IFACE_1 76 | 77 | rm -f $PTP_PHC2SYS_PID* $PTP_PTP4L_PID 78 | } 79 | 80 | monitor_ptp() 81 | { 82 | journalctl -xef | grep "phc2sys\|ptp4l" 83 | } 84 | 85 | usage() 86 | { 87 | echo "Usage: $0 {start|stop|log}" 88 | } 89 | case "$1" in 90 | start) 91 | start_ptp 92 | ;; 93 | stop) 94 | stop_ptp 95 | ;; 96 | log) 97 | monitor_ptp 98 | ;; 99 | *) 100 | usage 101 | exit 1 102 | ;; 103 | esac 104 | 105 | exit 0 106 | -------------------------------------------------------------------------------- /ptp/ptp4l.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | # 3 | # Default Data Set 4 | # 5 | twoStepFlag 1 6 | slaveOnly 1 7 | priority1 129 8 | priority2 129 9 | domainNumber 88 10 | clockClass 248 11 | clockAccuracy 0xFE 12 | offsetScaledLogVariance 0xFFFF 13 | free_running 0 14 | freq_est_interval 1 15 | dscp_event 0 16 | dscp_general 0 17 | # 18 | # Port Data Set 19 | # 20 | logAnnounceInterval 0 21 | logSyncInterval -3 22 | logMinDelayReqInterval -3 23 | logMinPdelayReqInterval -3 24 | announceReceiptTimeout 3 25 | syncReceiptTimeout 8 26 | delayAsymmetry 0 27 | fault_reset_interval 4 28 | neighborPropDelayThresh 20000000 29 | # 30 | # Run time options 31 | # 32 | assume_two_step 0 33 | logging_level 6 34 | path_trace_enabled 0 35 | follow_up_info 0 36 | hybrid_e2e 0 37 | tx_timestamp_timeout 1 38 | use_syslog 1 39 | verbose 0 40 | summary_interval 4 41 | kernel_leap 1 42 | check_fup_sync 0 43 | # 44 | # Servo Options 45 | # 46 | pi_proportional_const 0.0 47 | pi_integral_const 0.0 48 | pi_proportional_scale 0.0 49 | pi_proportional_exponent -0.3 50 | pi_proportional_norm_max 0.7 51 | pi_integral_scale 0.0 52 | pi_integral_exponent 0.4 53 | pi_integral_norm_max 0.3 54 | step_threshold 0.0 55 | first_step_threshold 0.00002 56 | max_frequency 900000000 57 | clock_servo pi 58 | sanity_freq_limit 200000000 59 | ntpshm_segment 0 60 | # 61 | # Transport options 62 | # 63 | transportSpecific 0x0 64 | ptp_dst_mac 01:1B:19:00:00:00 65 | p2p_dst_mac 01:80:C2:00:00:0E 66 | udp_ttl 1 67 | udp6_scope 0x0E 68 | uds_address /var/run/ptp4l 69 | # 70 | # Default interface options 71 | # 72 | network_transport UDPv4 73 | delay_mechanism E2E 74 | time_stamping hardware 75 | tsproc_mode filter 76 | delay_filter moving_median 77 | delay_filter_length 10 78 | egressLatency 0 79 | ingressLatency 0 80 | boundary_clock_jbod 0 81 | # 82 | # Clock description 83 | # 84 | productDescription ;; 85 | revisionData ;; 86 | manufacturerIdentity 00:00:00 87 | userDescription ; 88 | timeSource 0xA0 89 | -------------------------------------------------------------------------------- /ptp/ptp4l_lab.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | # 3 | # Default Data Set 4 | # 5 | twoStepFlag 1 6 | slaveOnly 1 7 | priority1 10 8 | priority2 10 9 | domainNumber 88 10 | clockClass 248 11 | clockAccuracy 0xFE 12 | offsetScaledLogVariance 0xFFFF 13 | free_running 0 14 | freq_est_interval 1 15 | dscp_event 0 16 | dscp_general 0 17 | # 18 | # Port Data Set 19 | # 20 | logAnnounceInterval 0 21 | logSyncInterval -3 22 | logMinDelayReqInterval -3 23 | logMinPdelayReqInterval -3 24 | announceReceiptTimeout 3 25 | syncReceiptTimeout 8 26 | delayAsymmetry 0 27 | fault_reset_interval 4 28 | neighborPropDelayThresh 20000000 29 | # 30 | # Run time options 31 | # 32 | assume_two_step 0 33 | logging_level 6 34 | path_trace_enabled 0 35 | follow_up_info 0 36 | hybrid_e2e 0 37 | tx_timestamp_timeout 1 38 | use_syslog 1 39 | verbose 0 40 | summary_interval 0 41 | kernel_leap 1 42 | check_fup_sync 0 43 | # 44 | # Servo Options 45 | # 46 | pi_proportional_const 0.0 47 | pi_integral_const 0.0 48 | pi_proportional_scale 0.0 49 | pi_proportional_exponent -0.3 50 | pi_proportional_norm_max 0.7 51 | pi_integral_scale 0.0 52 | pi_integral_exponent 0.4 53 | pi_integral_norm_max 0.3 54 | step_threshold 0.0 55 | first_step_threshold 0.00002 56 | max_frequency 900000000 57 | clock_servo pi 58 | sanity_freq_limit 200000000 59 | ntpshm_segment 0 60 | # 61 | # Transport options 62 | # 63 | transportSpecific 0x0 64 | ptp_dst_mac 01:1B:19:00:00:00 65 | p2p_dst_mac 01:80:C2:00:00:0E 66 | udp_ttl 1 67 | udp6_scope 0x0E 68 | uds_address /var/run/ptp4l 69 | # 70 | # Default interface options 71 | # 72 | network_transport UDPv4 73 | delay_mechanism E2E 74 | time_stamping hardware 75 | tsproc_mode filter 76 | delay_filter moving_median 77 | delay_filter_length 10 78 | egressLatency 0 79 | ingressLatency 0 80 | boundary_clock_jbod 0 81 | # 82 | # Clock description 83 | # 84 | productDescription ;; 85 | revisionData ;; 86 | manufacturerIdentity 00:00:00 87 | userDescription ; 88 | timeSource 0xA0 89 | -------------------------------------------------------------------------------- /ptp/ptp4l_orig.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | # 3 | # Default Data Set 4 | # 5 | twoStepFlag 1 6 | slaveOnly 0 7 | priority1 128 8 | priority2 128 9 | domainNumber 0 10 | #utc_offset 37 11 | clockClass 248 12 | clockAccuracy 0xFE 13 | offsetScaledLogVariance 0xFFFF 14 | free_running 0 15 | freq_est_interval 1 16 | dscp_event 0 17 | dscp_general 0 18 | # 19 | # Port Data Set 20 | # 21 | logAnnounceInterval 1 22 | logSyncInterval 0 23 | logMinDelayReqInterval 0 24 | logMinPdelayReqInterval 0 25 | announceReceiptTimeout 3 26 | syncReceiptTimeout 0 27 | delayAsymmetry 0 28 | fault_reset_interval 4 29 | neighborPropDelayThresh 20000000 30 | # 31 | # Run time options 32 | # 33 | assume_two_step 0 34 | logging_level 6 35 | path_trace_enabled 0 36 | follow_up_info 0 37 | hybrid_e2e 0 38 | tx_timestamp_timeout 1 39 | use_syslog 1 40 | verbose 0 41 | summary_interval 0 42 | kernel_leap 1 43 | check_fup_sync 0 44 | # 45 | # Servo Options 46 | # 47 | pi_proportional_const 0.0 48 | pi_integral_const 0.0 49 | pi_proportional_scale 0.0 50 | pi_proportional_exponent -0.3 51 | pi_proportional_norm_max 0.7 52 | pi_integral_scale 0.0 53 | pi_integral_exponent 0.4 54 | pi_integral_norm_max 0.3 55 | step_threshold 0.0 56 | first_step_threshold 0.00002 57 | max_frequency 900000000 58 | clock_servo pi 59 | sanity_freq_limit 200000000 60 | ntpshm_segment 0 61 | # 62 | # Transport options 63 | # 64 | transportSpecific 0x0 65 | ptp_dst_mac 01:1B:19:00:00:00 66 | p2p_dst_mac 01:80:C2:00:00:0E 67 | udp_ttl 1 68 | udp6_scope 0x0E 69 | uds_address /var/run/ptp4l 70 | # 71 | # Default interface options 72 | # 73 | network_transport UDPv4 74 | delay_mechanism E2E 75 | time_stamping hardware 76 | tsproc_mode filter 77 | delay_filter moving_median 78 | delay_filter_length 10 79 | egressLatency 0 80 | ingressLatency 0 81 | boundary_clock_jbod 0 82 | # 83 | # Clock description 84 | # 85 | productDescription ;; 86 | revisionData ;; 87 | manufacturerIdentity 00:00:00 88 | userDescription ; 89 | timeSource 0xA0 90 | -------------------------------------------------------------------------------- /ptp/ptp4l_st2110.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | # 3 | # Default Data Set 4 | # 5 | twoStepFlag 1 6 | slaveOnly 1 7 | priority1 128 8 | priority2 128 9 | domainNumber 88 10 | #utc_offset 37 11 | clockClass 248 12 | clockAccuracy 0xFE 13 | offsetScaledLogVariance 0xFFFF 14 | free_running 0 15 | freq_est_interval 1 16 | dscp_event 0 17 | dscp_general 0 18 | # 19 | # Port Data Set 20 | # 21 | logAnnounceInterval 0 22 | logSyncInterval -3 23 | logMinDelayReqInterval -3 24 | logMinPdelayReqInterval 0 25 | announceReceiptTimeout 3 26 | syncReceiptTimeout 0 27 | delayAsymmetry 0 28 | fault_reset_interval 4 29 | neighborPropDelayThresh 20000000 30 | # 31 | # Run time options 32 | # 33 | assume_two_step 0 34 | logging_level 6 35 | path_trace_enabled 0 36 | follow_up_info 0 37 | hybrid_e2e 0 38 | tx_timestamp_timeout 1 39 | use_syslog 1 40 | verbose 0 41 | summary_interval 0 42 | kernel_leap 1 43 | check_fup_sync 0 44 | # 45 | # Servo Options 46 | # 47 | pi_proportional_const 0.0 48 | pi_integral_const 0.0 49 | pi_proportional_scale 0.0 50 | pi_proportional_exponent -0.3 51 | pi_proportional_norm_max 0.7 52 | pi_integral_scale 0.0 53 | pi_integral_exponent 0.4 54 | pi_integral_norm_max 0.3 55 | step_threshold 0.0 56 | first_step_threshold 0.00002 57 | max_frequency 900000000 58 | clock_servo pi 59 | sanity_freq_limit 200000000 60 | ntpshm_segment 0 61 | # 62 | # Transport options 63 | # 64 | transportSpecific 0x0 65 | ptp_dst_mac 01:1B:19:00:00:00 66 | p2p_dst_mac 01:80:C2:00:00:0E 67 | udp_ttl 1 68 | udp6_scope 0x0E 69 | uds_address /var/run/ptp4l 70 | # 71 | # Default interface options 72 | # 73 | network_transport UDPv4 74 | delay_mechanism E2E 75 | time_stamping hardware 76 | tsproc_mode filter 77 | delay_filter moving_median 78 | delay_filter_length 10 79 | egressLatency 0 80 | ingressLatency 0 81 | boundary_clock_jbod 0 82 | # 83 | # Clock description 84 | # 85 | productDescription ;; 86 | revisionData ;; 87 | manufacturerIdentity 00:00:00 88 | userDescription ; 89 | timeSource 0xA0 90 | -------------------------------------------------------------------------------- /ptp/undesired_mgt_msg.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkeroulas/st2110-toolkit/f5a18617f9d4ee8bfe8fd5bed6de765cb8d42465/ptp/undesired_mgt_msg.pcapng -------------------------------------------------------------------------------- /transcoder/Dockerfile: -------------------------------------------------------------------------------- 1 | # transcoder image 2 | FROM centos:latest 3 | 4 | RUN adduser --uid 1000 --home /home/transcoder transcoder 5 | WORKDIR /home/transcoder/ 6 | 7 | RUN yum -y update && yum install -y git 8 | 9 | RUN git clone https://github.com/pkeroulas/st2110-toolkit.git 10 | RUN source st2110-toolkit/install.sh && \ 11 | install_common_tools && \ 12 | install_yasm && \ 13 | install_nasm && \ 14 | install_x264 && \ 15 | install_fdkaac && \ 16 | install_mp3 && \ 17 | install_ffmpeg 18 | -------------------------------------------------------------------------------- /transcoder/README.md: -------------------------------------------------------------------------------- 1 | # Transcode 2 | 3 | ## Use case 4 | 5 | The primary goal is to decode a large format uncompressed video signal 6 | (SMPTE ST 2110-20) along with raw audio (SMPTE ST 2110-30); the stream 7 | synchronization being based on PTP (SMPTE ST 2110-10). After the AV 8 | content is reconstructed, `ffmpeg` re-encodes the signal for storage or 9 | streaming. The project also aims at evaluating the limitation in terms 10 | of bandwidth, especially when additional streams are provided. 11 | 12 | ``` 13 | +-----------------------+ +-------------------+ +------------+ 14 | | Source: | | Transcoder: | | Monitor: | 15 | +-----------------------+ +-------------------+ +------------+ 16 | | exple: Gstreamer or HW| | ffmpeg | | vlc, ffplay| 17 | +-----------------------+ +-------------------+ +------------+ 18 | | generate rtp streams |-- video -->| depacketize, | h264 | | 19 | | | RFC 4175 | reconstruct, |-- mpeg-ts -->| playback | 20 | | |-- audio -->| encode and stream | udp/srt | | 21 | +-----------------------+ AES67 +-------------------+ +------------+ 22 | ``` 23 | 24 | Other tools like `gstreamer` may be used as a transcoder but the following 25 | study focuses on `ffmpeg`. 26 | 27 | ## Install ffmpeg and dependencies 28 | 29 | From top directory, 30 | 31 | ``` 32 | sudo -i 33 | ./install.sh common # gcc, libtool, tar etc. 34 | ./install.sh transcoder # depencies (yasm, nasm, x264, fdkaac, mp3, srt) and ffmpeg 35 | ``` 36 | 37 | ## Get a SMPTE 2110 source 38 | 39 | If you don't have any source, consider using a [sofware-based source](../doc/SW_source.md) 40 | for testing. 41 | 42 | A SMPTE 2110 streams are described by an individual SDP file for each 43 | essence: video, audio, ancillary. However, ffmpeg hardly takes multiple 44 | SDP files as input. A hack consists in combining the essence 45 | descriptions in one single SDP. See [example](../doc/sdp.sample), where 46 | audio is declared before video to ensure that ffmpeg process this 47 | lighter stream first. 48 | 49 | ## Simple test 50 | 51 | Transcode AV streams to h264 low res file: 52 | 53 | ```sh 54 | ffmpeg -loglevel debug \ 55 | -buffer_size 671088640 -protocol_whitelist file,udp,rtp -i \ 56 | -fifo_size 1000000000 \ 57 | -c:a libfdk_aac -ac 2 \ 58 | -vf scale=640:480 \ 59 | -c:v libx264 -preset ultrafast -pass 1 \ 60 | output.mp4 61 | ``` 62 | 63 | Decode interlaced and re-stream to h264 @ 2.5Mbps over mpegts: 64 | 65 | ```sh 66 | ffmpeg -loglevel debug \ 67 | -buffer_size 671088640 -protocol_whitelist file,udp,rtp -i \ 68 | -fifo_size 1000000000 -passlogfile /tmp/ffmpeg2pass \ 69 | -c:a libfdk_aac -ac 2 -b:a 128k \ 70 | -r 30 -vf yadif=0:-1:0 \ 71 | -s 1280x720 -pix_fmt yuv420p -c:v libx264 -profile:v main -preset fast \ 72 | -level:v 3.1 -b:v 2500k -x264-params b-pyramid=1 -g 30 -keyint_min 16 -pass 1 -refs 6 \ 73 | -f mpegts udp://:5000 74 | ``` 75 | 76 | On a monitoring host: 77 | 78 | ```sh 79 | ffplay udp://@0.0.0.0:5000 80 | vlc --network-caching 4000 udp://@0.0.0.0:5000 81 | ``` 82 | 83 | If no packets are received, refer to the [troubleshoot guide](../doc/troubleshoot.md). 84 | 85 | ## RTP packet drops 86 | 87 | This is the most common error you'll get. 88 | `ffmpeg` may complain about RTP discontinuity with messages including: 89 | 90 | ``` 91 | jitter buffer full 92 | RTP: missed ******* packets 93 | Missed previous RTP Marker 94 | RTP: dropping old packet received too late 95 | ``` 96 | 97 | This is most likely due to the input buffer being too small or the 98 | transcode process creating a bottleneck that prevents ffpmeg from 99 | reading the incomming packets fast enough. Here are some of the knobs 100 | you can try to adjust: 101 | 102 | - downscale the image resolution 103 | - change CPU, see [performance analysis](../doc/transcoder_perf.md) 104 | - force multithread by applying this [patch](ffmpeg-force-input-threading.patch) 105 | - tune up your network stack, see optimization section below 106 | - strip the command to bare minimum and start from here to : 107 | 108 | ``` 109 | ffmpeg -y -loglevel verbose -buffer_size 671088640 -protocol_whitelist 'file,udp,rtp' -i mysdp.sdp -f null /dev/null 110 | ``` 111 | 112 | ## Start as a service 113 | 114 | Once the process is stable, you can start it as a background task. 115 | Use ./transcode.sh to start the transcoding service in background from 116 | one or multiple SDP files, then show logs and, finally stop the service. 117 | 118 | ```sh 119 | $ ./transcoder.sh help 120 | [...] 121 | $ ./transcoder.sh start file.sdp 122 | ==================== Start ... ==================== 123 | Transcoding from file.sdp 124 | Scaling and encoding: cpu. 125 | Audio codec is aac. 126 | [...] 127 | $ ./transcoder.sh log 128 | [...] 129 | $ ./transcoder.sh stop 130 | ==================== Stop ... ==================== 131 | ``` 132 | 133 | Script constants, like destination IP, can be overridden by conf file, 134 | i.e. `/etc/st2110.conf`. See sample `./config/st2110.conf` for details. 135 | 136 | ## Optimization 137 | 138 | ### System network stack 139 | 140 | You can also check that your NIC ring buffer is the largest as possible: 141 | 142 | ``` 143 | sudo ethtool -g 144 | sudo ethtool -G 8192 145 | ``` 146 | 147 | And consider increasing Kernel Rx buffer: 148 | 149 | ``` 150 | sysctl net.core.rmem_max=671088640 151 | sysctl net.core.rmem_default=671088640 152 | echo 10000 > /proc/sys/net/core/netdev_max_backlog 153 | ``` 154 | 155 | Verify the memory usage of your receiving socket: 156 | 157 | ``` 158 | ss -uamp | grep -A1 ffmpeg 159 | 160 | UNCONN 1119285120 0 225.164.14.100:20000 0.0.0.0:* users:(("ffmpeg",pid 161 | # ^ bytes of data have been received by the kernel but haven’t yet been copied by the process 162 | skmem:(r1016405760,rb1342177280,t0,tb212992,f256,w0,o112,bl0,d75818) 163 | # ^ ^max ^pkt drops 164 | # | current, should ideally be 0 165 | ``` 166 | 167 | Use `./transcoder/transcoder_stats.sh` for complete stats. 168 | 169 | ### FFmpeg options 170 | 171 | RTP Input: 172 | 173 | * `-reorder_queue_size`: jitter buffer size, default is 500 pkts, not relevant if no reordering expected 174 | * `-buffer_size`: socket memory size in the kernel, overrides /proc/sys/net/core/rmem_default by setsockopt() 175 | * `-max_delay` 176 | * `-fifo_size` 177 | 178 | See usage: `ss -uamp | grep ffmpeg -A1` 179 | 180 | Raw: 181 | 182 | * `-thread_queue_size`: 8 AVPackets available by default 183 | * `-vf yadif=0:-1:0`: for de-interlacing 184 | 185 | Output: 186 | 187 | `-pass 1` (h264): without this option the CPU usage is way higher and 188 | the audio breaks after a few seconds, at least for rtmp output. The 1st 189 | con of the option is that the output bitrate might less precise. And the 190 | generated passlog file is quite large (~10GB/day). The 'monitor' 191 | function of the transcoder checks the size of this file and restarts 192 | ffmpeg if needed. 193 | 194 | ### Hardware acceleration for transcoding 195 | 196 | [Nvidia setup.](../doc/hw_encoding.md) 197 | 198 | ### Transcoding performance 199 | 200 | [Measurements](../doc/transcoder_perf.md) with CPU vs GPU. 201 | 202 | ## FFmpeg files 203 | 204 | The demux is composed of: 205 | 206 | * `libavformat/sdp` 207 | * `libavformat/udp` (multicast join) 208 | * `libavformat/rtsp` 209 | * `libavformat/rtpdec` 210 | * `libavformat/rtpdec_rfc4175` (dynamic RTP handler) 211 | * `libavcodec/bitpacked_dec` 212 | 213 | ## Limitations 214 | 215 | ### Network redundancy 216 | 217 | SMPTE 2022-7 is not supported. Eventhough, an SDP with dual stream will 218 | be correctly processed, the output would contains 2 separated tracks. 219 | 220 | ### A/V synchro 221 | 222 | Audio and video are transcoded as packets come in with no regard to 223 | their respective RTP timestamps. If temporal realignement is important, 224 | and if RTP timestamps are reliable, consider applying the following 225 | patches before re-compile: 226 | 227 | * ffmpeg-avutil-smpte2110-add-helpers-to-compute-PTS.patch 228 | * ffmpeg-avformat-rtp-compute-smpte2110-timestamps.patch 229 | 230 | And activate in command line: `-smpte2110_timestamp 1` 231 | 232 | ### Trancoding ancillary data (SMPTE ST 2110-40) 233 | 234 | There are some limitations in [transcoding closed 235 | caption](../doc/closed_captions.md). 236 | 237 | Here are some guidelines for [SCTE-35](../doc/scte_104_to_35.md). 238 | -------------------------------------------------------------------------------- /transcoder/ffmpeg-avformat-rtp-compute-smpte2110-timestamps.patch: -------------------------------------------------------------------------------- 1 | From 809569272a6d440add528139aba20f401a0fb963 Mon Sep 17 00:00:00 2001 2 | From: Damien Riegel 3 | Date: Thu, 22 Feb 2018 14:33:00 -0500 4 | Subject: [PATCH 2/3] avformat/rtp: compute smpte2110 timestamps 5 | 6 | If the `-smpte2110_timestamp 1` is passed on the command line to the 7 | RTP demuxer, and the RTP demuxers don't set PTS on the AVPacket they 8 | return, then the PTS will be computed according to the SMPTE2110 9 | standard, using the RTP timestamp. 10 | --- 11 | libavformat/rtpdec.c | 16 ++++++++++++++++ 12 | libavformat/rtpdec.h | 4 ++++ 13 | libavformat/rtsp.c | 9 ++++++--- 14 | libavformat/rtsp.h | 5 +++++ 15 | 4 files changed, 31 insertions(+), 3 deletions(-) 16 | 17 | diff --git a/libavformat/rtpdec.c b/libavformat/rtpdec.c 18 | index 8d4532ec30..cf76e8a5c0 100644 19 | --- a/libavformat/rtpdec.c 20 | +++ b/libavformat/rtpdec.c 21 | @@ -633,6 +633,13 @@ static void finalize_packet(RTPDemuxContext *s, AVPacket *pkt, uint32_t timestam 22 | if (timestamp == RTP_NOTS_VALUE) 23 | return; 24 | 25 | + if (s->smpte2110_ts) { 26 | + pkt->pts = smpte2110_compute_pts(s->ic, s->smpte2110_ts, timestamp, 27 | + s->st->time_base); 28 | + if (pkt->pts != AV_NOPTS_VALUE) 29 | + return; 30 | + } 31 | + 32 | if (s->last_rtcp_ntp_time != AV_NOPTS_VALUE && s->ic->nb_streams > 1) { 33 | int64_t addend; 34 | int delta_timestamp; 35 | @@ -757,6 +764,15 @@ void ff_rtp_reset_packet_queue(RTPDemuxContext *s) 36 | s->prev_ret = 0; 37 | } 38 | 39 | +int ff_rtp_enable_smpte2110_timestamp(RTPDemuxContext *s) 40 | +{ 41 | + s->smpte2110_ts = smpte2110_alloc(); 42 | + if (!s->smpte2110_ts) 43 | + return AVERROR(ENOMEM); 44 | + 45 | + return 0; 46 | +} 47 | + 48 | static int enqueue_packet(RTPDemuxContext *s, uint8_t *buf, int len) 49 | { 50 | uint16_t seq = AV_RB16(buf + 2); 51 | diff --git a/libavformat/rtpdec.h b/libavformat/rtpdec.h 52 | index e1ced132db..ae1337dcf4 100644 53 | --- a/libavformat/rtpdec.h 54 | +++ b/libavformat/rtpdec.h 55 | @@ -24,6 +24,7 @@ 56 | #define AVFORMAT_RTPDEC_H 57 | 58 | #include "libavcodec/avcodec.h" 59 | +#include "libavutil/smpte2110.h" 60 | #include "avformat.h" 61 | #include "rtp.h" 62 | #include "url.h" 63 | @@ -51,6 +52,7 @@ int ff_rtp_parse_packet(RTPDemuxContext *s, AVPacket *pkt, 64 | void ff_rtp_parse_close(RTPDemuxContext *s); 65 | int64_t ff_rtp_queued_packet_time(RTPDemuxContext *s); 66 | void ff_rtp_reset_packet_queue(RTPDemuxContext *s); 67 | +int ff_rtp_enable_smpte2110_timestamp(RTPDemuxContext *s); 68 | 69 | /** 70 | * Send a dummy packet on both port pairs to set up the connection 71 | @@ -190,6 +192,8 @@ struct RTPDemuxContext { 72 | 73 | /* packet loss tracking */ 74 | uint64_t rtp_packets_missed; 75 | + 76 | + struct smpte2110_timestamp *smpte2110_ts; 77 | }; 78 | 79 | /** 80 | diff --git a/libavformat/rtsp.c b/libavformat/rtsp.c 81 | index 25bdf475b3..d911735ce1 100644 82 | --- a/libavformat/rtsp.c 83 | +++ b/libavformat/rtsp.c 84 | @@ -74,8 +74,8 @@ 85 | #define COMMON_OPTS() \ 86 | { "reorder_queue_size", "set number of packets to buffer for handling of reordered packets", OFFSET(reordering_queue_size), AV_OPT_TYPE_INT, { .i64 = -1 }, -1, INT_MAX, DEC }, \ 87 | { "buffer_size", "Underlying protocol send/receive buffer size", OFFSET(buffer_size), AV_OPT_TYPE_INT, { .i64 = -1 }, -1, INT_MAX, DEC|ENC }, \ 88 | - { "pkt_size", "Underlying protocol send packet size", OFFSET(pkt_size), AV_OPT_TYPE_INT, { .i64 = -1 }, -1, INT_MAX, ENC } \ 89 | - 90 | + { "pkt_size", "Underlying protocol send packet size", OFFSET(pkt_size), AV_OPT_TYPE_INT, { .i64 = -1 }, -1, INT_MAX, ENC }, \ 91 | + { "smpte2110_timestamp", "Compute PTS based on RTP timestamps, according to SMPTE2110 spec", OFFSET(compute_smpte2110_timestamp), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, DEC} 92 | 93 | const AVOption ff_rtsp_options[] = { 94 | { "initial_pause", "do not start playing the stream immediately", OFFSET(initial_pause), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DEC }, 95 | @@ -859,10 +859,13 @@ int ff_rtsp_open_transport_ctx(AVFormatContext *s, RTSPStream *rtsp_st) 96 | rtsp_st->transport_priv = ff_rdt_parse_open(s, st->index, 97 | rtsp_st->dynamic_protocol_context, 98 | rtsp_st->dynamic_handler); 99 | - else if (CONFIG_RTPDEC) 100 | + else if (CONFIG_RTPDEC) { 101 | rtsp_st->transport_priv = ff_rtp_parse_open(s, st, 102 | rtsp_st->sdp_payload_type, 103 | reordering_queue_size); 104 | + if (rt->compute_smpte2110_timestamp) 105 | + ff_rtp_enable_smpte2110_timestamp(rtsp_st->transport_priv); 106 | + } 107 | 108 | if (!rtsp_st->transport_priv) { 109 | return AVERROR(ENOMEM); 110 | diff --git a/libavformat/rtsp.h b/libavformat/rtsp.h 111 | index 1310dd9c08..7a3d13b8cd 100644 112 | --- a/libavformat/rtsp.h 113 | +++ b/libavformat/rtsp.h 114 | @@ -419,6 +419,11 @@ typedef struct RTSPState { 115 | char default_lang[4]; 116 | int buffer_size; 117 | int pkt_size; 118 | + 119 | + /** 120 | + * Derive PTS from the RTP timestamp, according to spec SMPTE2110 121 | + */ 122 | + int compute_smpte2110_timestamp; 123 | } RTSPState; 124 | 125 | #define RTSP_FLAG_FILTER_SRC 0x1 /**< Filter incoming UDP packets - 126 | -- 127 | 2.25.1 128 | 129 | -------------------------------------------------------------------------------- /transcoder/ffmpeg-avutil-smpte2110-add-helpers-to-compute-PTS.patch: -------------------------------------------------------------------------------- 1 | From 9cbdf55fdbf67d97ca6aea7ca152ff8da75e78d3 Mon Sep 17 00:00:00 2001 2 | From: Damien Riegel 3 | Date: Thu, 22 Feb 2018 14:29:16 -0500 4 | Subject: [PATCH 1/3] avutil/smpte2110: add helpers to compute PTS 5 | 6 | With the SMPTE2110 standard, sampling instants are "encoded" in RTP 7 | timestamps. `smpte2110_compute_pts` aims at recomputing back sampling 8 | instants from the RTP timestamps. 9 | --- 10 | libavutil/Makefile | 2 + 11 | libavutil/smpte2110.c | 149 ++++++++++++++++++++++++++++++++++++++++++ 12 | libavutil/smpte2110.h | 37 +++++++++++ 13 | 3 files changed, 188 insertions(+) 14 | create mode 100644 libavutil/smpte2110.c 15 | create mode 100644 libavutil/smpte2110.h 16 | 17 | diff --git a/libavutil/Makefile b/libavutil/Makefile 18 | index 664c9d8b77..25accf23b5 100644 19 | --- a/libavutil/Makefile 20 | +++ b/libavutil/Makefile 21 | @@ -70,6 +70,7 @@ HEADERS = adler32.h \ 22 | replaygain.h \ 23 | ripemd.h \ 24 | samplefmt.h \ 25 | + smpte2110.h \ 26 | sha.h \ 27 | sha512.h \ 28 | spherical.h \ 29 | @@ -154,6 +155,7 @@ OBJS = adler32.o \ 30 | rc4.o \ 31 | ripemd.o \ 32 | samplefmt.o \ 33 | + smpte2110.o \ 34 | sha.o \ 35 | sha512.o \ 36 | slicethread.o \ 37 | diff --git a/libavutil/smpte2110.c b/libavutil/smpte2110.c 38 | new file mode 100644 39 | index 0000000000..f4d2767e19 40 | --- /dev/null 41 | +++ b/libavutil/smpte2110.c 42 | @@ -0,0 +1,149 @@ 43 | +/* 44 | + * Utilities for SMPTE ST 2110 decoding 45 | + * Copyright (c) 2018 Savoir-faire Linux, Inc 46 | + * 47 | + * This file is part of FFmpeg. 48 | + * 49 | + * FFmpeg is free software; you can redistribute it and/or 50 | + * modify it under the terms of the GNU Lesser General Public 51 | + * License as published by the Free Software Foundation; either 52 | + * version 2.1 of the License, or (at your option) any later version. 53 | + * 54 | + * FFmpeg is distributed in the hope that it will be useful, 55 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of 56 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 57 | + * Lesser General Public License for more details. 58 | + * 59 | + * You should have received a copy of the GNU Lesser General Public 60 | + * License along with FFmpeg; if not, write to the Free Software 61 | + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 62 | + */ 63 | + 64 | +/* Development sponsored by CBC/Radio-Canada */ 65 | + 66 | +#include "common.h" 67 | +#include "time.h" 68 | + 69 | +#include "libavformat/avformat.h" 70 | + 71 | +#include "smpte2110.h" 72 | + 73 | +struct smpte2110_timestamp { 74 | + int64_t last_sync; 75 | + int64_t previous_timestamp; 76 | +}; 77 | + 78 | + 79 | +#define USEC_IN_SEC 1000000LL 80 | +static int64_t time_to_timebase(int64_t time, AVRational timebase) 81 | +{ 82 | + return (time / USEC_IN_SEC) * timebase.den / timebase.num + 83 | + (time % USEC_IN_SEC) * (timebase.den / timebase.num) / USEC_IN_SEC; 84 | +} 85 | +#undef USEC_IN_SEC 86 | + 87 | +#define RTP_TIMESTAMP_WRAP (1LL << 32) 88 | + 89 | +struct smpte2110_timestamp* smpte2110_alloc(void) 90 | +{ 91 | + return av_mallocz(sizeof(struct smpte2110_timestamp)); 92 | +} 93 | + 94 | +static int __smpte2110_compute_pts(void *avlc, int64_t *last_sync, 95 | + int64_t *computed_pts, 96 | + uint32_t previous_timestamp, 97 | + uint32_t current_timestamp, 98 | + AVRational time_base) 99 | +{ 100 | + int64_t last_sync_point = *last_sync; 101 | + int64_t pts; 102 | + 103 | + /* if we failed to compute the base time, there is no need to keep trying */ 104 | + if (last_sync_point == AV_NOPTS_VALUE) 105 | + return AVERROR(EINVAL); 106 | + 107 | + if (!last_sync_point) { 108 | + int64_t current_time = av_gettime(); 109 | + int64_t now = time_to_timebase(current_time, time_base); 110 | + int64_t wrap_detect; 111 | + 112 | + last_sync_point = (now / RTP_TIMESTAMP_WRAP) * RTP_TIMESTAMP_WRAP; 113 | + 114 | + pts = last_sync_point + current_timestamp; 115 | + 116 | + /* 117 | + * last 118 | + * sync now wrap timestamp 119 | + * |-----|------|------|--------|------------> time 120 | + * 121 | + * Last sync point is derived from the current time, but the timestamp we 122 | + * get might be just after a wrap, so its value would be very low and 123 | + * last_sync + timestamp would be way before "now". Let's try to detect 124 | + * that and move the last sync point to the next occurrence. 125 | + * 126 | + * The opposite situation, where timestamp < wrap < now, is also 127 | + * possible. In that case, move the last sync point back. 128 | + */ 129 | + 130 | + wrap_detect = av_rescale(600, time_base.den, time_base.num); 131 | + if (pts > now && (pts - now) > wrap_detect) { 132 | + last_sync_point -= RTP_TIMESTAMP_WRAP; 133 | + } else if (now > pts && (now - pts) > wrap_detect) { 134 | + last_sync_point += RTP_TIMESTAMP_WRAP; 135 | + } 136 | + 137 | + pts = last_sync_point + current_timestamp; 138 | + 139 | + /* 140 | + * Check that after a potential wrap was taken into account, we computed 141 | + * a pts value that is "close enough" of the current time. If it is 142 | + * still too far, give up and let common code timestamp the frames with 143 | + * another method. 144 | + */ 145 | + if (FFABS(now - pts) > wrap_detect) { 146 | + av_log(avlc, AV_LOG_WARNING, "Unable to determine base time\n"); 147 | + *last_sync = AV_NOPTS_VALUE; 148 | + return AVERROR(EINVAL); 149 | + } else { 150 | + av_log(avlc, AV_LOG_DEBUG, "now: %" PRId64 "\n", now); 151 | + av_log(avlc, AV_LOG_DEBUG, "last_sync: %" PRId64 "\n", last_sync_point); 152 | + av_log(avlc, AV_LOG_DEBUG, "RTP timestamp: %" PRId32 "\n", current_timestamp); 153 | + av_log(avlc, AV_LOG_DEBUG, "wrap in: %" PRId64 "s\n", 154 | + ((int64_t)RTP_TIMESTAMP_WRAP - current_timestamp) / time_base.den); 155 | + av_log(avlc, AV_LOG_DEBUG, "pts: %" PRId64 "\n", pts); 156 | + av_log(avlc, AV_LOG_DEBUG, "(now - pts) / %dk: %" PRId64 "\n", 157 | + time_base.den / 1000, 158 | + (now - pts) / time_base.den); 159 | + } 160 | + 161 | + } else { 162 | + if (current_timestamp < previous_timestamp) { 163 | + last_sync_point += RTP_TIMESTAMP_WRAP; 164 | + av_log(avlc, AV_LOG_DEBUG, "PTS WRAP\n"); 165 | + } 166 | + 167 | + pts = last_sync_point + current_timestamp; 168 | + } 169 | + 170 | + *last_sync = last_sync_point; 171 | + *computed_pts = pts; 172 | + 173 | + return 0; 174 | +} 175 | + 176 | +int64_t smpte2110_compute_pts(void *avlc, struct smpte2110_timestamp *ts, 177 | + uint32_t current_timestamp, AVRational time_base) 178 | +{ 179 | + int64_t pts; 180 | + int ret; 181 | + 182 | + ret = __smpte2110_compute_pts(avlc, &ts->last_sync, &pts, 183 | + ts->previous_timestamp, current_timestamp, 184 | + time_base); 185 | + if (ret < 0) 186 | + pts = AV_NOPTS_VALUE; 187 | + 188 | + ts->previous_timestamp = current_timestamp; 189 | + 190 | + return pts; 191 | +} 192 | diff --git a/libavutil/smpte2110.h b/libavutil/smpte2110.h 193 | new file mode 100644 194 | index 0000000000..d2a70cf9eb 195 | --- /dev/null 196 | +++ b/libavutil/smpte2110.h 197 | @@ -0,0 +1,37 @@ 198 | +/* 199 | + * This file is part of FFmpeg. 200 | + * 201 | + * FFmpeg is free software; you can redistribute it and/or 202 | + * modify it under the terms of the GNU Lesser General Public 203 | + * License as published by the Free Software Foundation; either 204 | + * version 2.1 of the License, or (at your option) any later version. 205 | + * 206 | + * FFmpeg is distributed in the hope that it will be useful, 207 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of 208 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 209 | + * Lesser General Public License for more details. 210 | + * 211 | + * You should have received a copy of the GNU Lesser General Public 212 | + * License along with FFmpeg; if not, write to the Free Software 213 | + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 214 | + */ 215 | + 216 | +/** 217 | + * @file 218 | + * smpte2110 utils, functions that are useful for all essences of SMPTE-2110 219 | + */ 220 | + 221 | +#ifndef AVUTIL_SMPTE2110_H 222 | +#define AVUTIL_SMPTE2110_H 223 | + 224 | +#include "common.h" 225 | +#include "time.h" 226 | + 227 | +struct smpte2110_timestamp; 228 | + 229 | +struct smpte2110_timestamp* smpte2110_alloc(void); 230 | + 231 | +int64_t smpte2110_compute_pts(void *avlc, struct smpte2110_timestamp *ts, 232 | + uint32_t current_timestamp, AVRational time_base); 233 | + 234 | +#endif 235 | -- 236 | 2.25.1 237 | 238 | -------------------------------------------------------------------------------- /transcoder/ffmpeg-force-input-threading.patch: -------------------------------------------------------------------------------- 1 | From e4fe70eb4beb0cef616da790df00e47a94c7f1a7 Mon Sep 17 00:00:00 2001 2 | From: Patrick Keroulas 3 | Date: Fri, 16 Nov 2018 16:57:38 -0500 4 | Subject: ffmpeg: force input threading 5 | 6 | --- 7 | fftools/ffmpeg.c | 9 +++++++-- 8 | 1 file changed, 7 insertions(+), 2 deletions(-) 9 | 10 | diff --git a/fftools/ffmpeg.c b/fftools/ffmpeg.c 11 | index e7384f052a..a00709979d 100644 12 | --- a/fftools/ffmpeg.c 13 | +++ b/fftools/ffmpeg.c 14 | @@ -23,6 +23,8 @@ 15 | * multimedia converter based on the FFmpeg libraries 16 | */ 17 | 18 | +#define _GNU_SOURCE // needed for thread_setname_np 19 | + 20 | #include "config.h" 21 | #include 22 | #include 23 | @@ -3717,8 +3719,9 @@ static int init_input_thread(int i) 24 | int ret; 25 | InputFile *f = input_files[i]; 26 | 27 | + /* we want to force a seperate input thread even with only one input */ 28 | if (f->thread_queue_size < 0) 29 | - f->thread_queue_size = (nb_input_files > 1 ? 8 : 0); 30 | + f->thread_queue_size = 8; //(nb_input_files > 1 ? 8 : 0); 31 | if (!f->thread_queue_size) 32 | return 0; 33 | 34 | @@ -3735,6 +3738,7 @@ static int init_input_thread(int i) 35 | av_thread_message_queue_free(&f->in_thread_queue); 36 | return AVERROR(ret); 37 | } 38 | + pthread_setname_np(f->thread, "ffmpeg-input"); 39 | 40 | return 0; 41 | } 42 | @@ -3781,7 +3785,8 @@ static int get_input_packet(InputFile *f, AVPacket **pkt) 43 | } 44 | 45 | #if HAVE_THREADS 46 | - if (f->thread_queue_size) 47 | + /* we use a seperate input thread even with only one input */ 48 | + // if (f->thread_queue_size) 49 | return get_input_packet_mt(f, pkt); 50 | #endif 51 | *pkt = f->pkt; 52 | -- 53 | 2.25.1 54 | 55 | -------------------------------------------------------------------------------- /transcoder/install.sh: -------------------------------------------------------------------------------- 1 | 2 | export FDKAAC_VERSION=0.1.4 \ 3 | YASM_VERSION=1.3.0 \ 4 | NASM_VERSION=2.13.02 \ 5 | MP3_VERSION=3.99.5 \ 6 | FFMPEG_VERSION=5.1 \ 7 | MAKEFLAGS="-j$[$(nproc) + 1]" 8 | 9 | if [ -z $PACKAGE_MANAGER ]; then 10 | echo "!!! Don't execute this script directly !!! 11 | Usage: 12 | /install.sh transcoder" 13 | exit 1 14 | fi 15 | if [ -z $PREFIX ]; then 16 | echo "$PREFIX undefined. Set to default '/usr/local'" 17 | PREFIX=/usr/local 18 | fi 19 | if [ -z $PKG_CONFIG_PATH ]; then 20 | echo "$PKG_CONFIG_PATH undefined. Set to default." 21 | export PKG_CONFIG_PATH=${PREFIX}/lib/pkgconfig 22 | fi 23 | 24 | install_yasm() 25 | { 26 | echo "Installing YASM" 27 | DIR=$(mktemp -d) 28 | cd $DIR/ 29 | curl -s http://www.tortall.net/projects/yasm/releases/yasm-$YASM_VERSION.tar.gz | 30 | tar zxvf - -C . 31 | cd $DIR/yasm-$YASM_VERSION/ 32 | ./configure --prefix="$PREFIX" --bindir="$PREFIX/bin" --docdir=$DIR -mandir=$DIR 33 | make 34 | make install 35 | make distclean 36 | rm -rf $DIR 37 | } 38 | 39 | install_nasm() 40 | { 41 | echo "Installing NASM" 42 | if [ $PACKAGE_MANAGER = "yum" ]; then 43 | DIR=$(mktemp -d) 44 | cd $DIR/ 45 | nasm_rpm=nasm-$NASM_VERSION-0.fc24.x86_64.rpm 46 | curl -O https://www.nasm.us/pub/nasm/releasebuilds/$NASM_VERSION/linux/$nasm_rpm 47 | rpm -i $nasm_rpm 48 | rm -f $nasm_rpm 49 | rm -rf $DIR 50 | else 51 | $PACKAGE_MANAGER -y install nasm 52 | fi 53 | } 54 | 55 | install_x264() 56 | { 57 | echo "Installing x264" 58 | DIR=$(mktemp -d) 59 | cd $DIR/ 60 | git clone -b stable --single-branch http://git.videolan.org/git/x264.git 61 | cd x264/ 62 | ./configure --prefix="$PREFIX" --bindir="$PREFIX/bin" --enable-shared 63 | make 64 | make install 65 | make distclean 66 | rm -rf $DIR 67 | } 68 | 69 | install_fdkaac() 70 | { 71 | echo "Installing fdk-aac" 72 | DIR=$(mktemp -d) 73 | cd $DIR/ 74 | curl -s https://codeload.github.com/mstorsjo/fdk-aac/tar.gz/v$FDKAAC_VERSION | 75 | tar zxvf - -C . 76 | cd fdk-aac-$FDKAAC_VERSION/ 77 | autoreconf -fiv 78 | ./configure --prefix="$PREFIX" --disable-shared 79 | make CXXFLAGS="-std=gnu++98" # compatibility with gcc v7... 80 | make install 81 | make distclean 82 | rm -rf $DIR 83 | } 84 | 85 | install_mp3() 86 | { 87 | echo "Installing mp3" 88 | DIR=$(mktemp -d) 89 | cd $DIR/ 90 | curl -s -L http://downloads.sourceforge.net/project/lame/lame/3.99/lame-$MP3_VERSION.tar.gz | 91 | tar zxvf - -C . 92 | cd lame-$MP3_VERSION/ 93 | ./configure --prefix="$PREFIX" --bindir="$PREFIX/bin" --disable-shared --enable-nasm 94 | make 95 | make install 96 | make distclean 97 | rm -rf $DIR 98 | } 99 | 100 | install_ffnvcodec() 101 | { 102 | echo "Installing ffnvcodev" 103 | DIR=$(mktemp -d) 104 | cd $DIR/ 105 | git clone https://git.videolan.org/git/ffmpeg/nv-codec-headers.git 106 | cd nv-codec-headers 107 | make 108 | make install 109 | make distclean 110 | rm -rf $DIR 111 | # provide new option to ffmpeg 112 | ffmpeg_gpu_options="--enable-cuda --enable-cuvid --enable-nvenc --enable-libnpp --extra-cflags=-I$PREFIX/cuda/include --extra-ldflags=-L$PREFIX/cuda/lib64" 113 | } 114 | 115 | install_streaming_server() 116 | { 117 | $PACKAGE_MANAGER install nginx libnginx-mod-rtmp 118 | install -m 644 $TOP_DIR/config/nginx.conf /etc/nginx.conf 119 | } 120 | 121 | install_libsrt() 122 | { 123 | if [ $PACKAGE_MANAGER = "apt" ]; then 124 | $PACKAGE_MANAGER install -y libsrt-dev libsrt1 125 | else 126 | $PACKAGE_MANAGER install -y srt-devel srt-libs 127 | # FIXME Centos 7 has srt v1.2.3 whereas ffmpeg 5 requires 1.3 128 | # need to compile srt 129 | # https://github.com/Haivision/srt/blob/master/docs/build/build-linux.md 130 | fi 131 | } 132 | 133 | install_ffmpeg() 134 | { 135 | cd $TOP_DIR 136 | dir=$(pwd) 137 | 138 | ldconfig -v 139 | echo "Installing ffmpeg" 140 | DIR=$(mktemp -d) 141 | cd $DIR/ 142 | git clone https://git.ffmpeg.org/ffmpeg.git 143 | cd ffmpeg 144 | git checkout -b $FFMPEG_VERSION origin/release/$FFMPEG_VERSION 145 | 146 | patch -p1 < $dir/transcoder/ffmpeg-force-input-threading.patch 147 | #patch -p1 < $dir/transcoder/ffmpeg-avformat-rtp-compute-smpte2110-timestamps.patch 148 | #patch -p1 < $dir/transcoder/ffmpeg-ffmpeg-avformat-rtp-compute-smpte2110-timestamps.patch 149 | 150 | ./configure --prefix=$PREFIX \ 151 | --extra-cflags=-I$PREFIX/include \ 152 | --extra-ldflags=-L$PREFIX/lib \ 153 | --bindir=$PREFIX/bin \ 154 | --extra-libs=-ldl \ 155 | --enable-version3 --enable-gpl --enable-nonfree \ 156 | --enable-postproc --enable-libsrt \ 157 | --enable-libx264 --enable-libfdk-aac --enable-libmp3lame \ 158 | --disable-ffplay --disable-ffprobe \ 159 | ${ffmpeg_gpu_options-} \ 160 | --enable-small --disable-stripping --disable-debug 161 | 162 | make 163 | make install 164 | make distclean 165 | rm -rf $DIR 166 | } 167 | 168 | install_transcoder() 169 | { 170 | install_yasm 171 | install_nasm 172 | install_x264 173 | install_fdkaac 174 | install_mp3 175 | install_libsrt 176 | install_ffmpeg 177 | } 178 | -------------------------------------------------------------------------------- /transcoder/transcoder_stats.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | clear 4 | 5 | PROCESS="ffmpeg" 6 | 7 | show () { 8 | printf "%-20.20s %-20.20s %s \n" "$1" "$2" "$3" 9 | } 10 | 11 | show_all () { 12 | pid=$(pidof $PROCESS) 13 | 14 | if [ -z $pid ]; then 15 | echo $PROCESS not running, exit. 16 | exit 1 17 | fi 18 | process_stat=$(ps -p $pid -o comm,pcpu,pmem,etimes,etime,args) 19 | uptime_sec=$(echo "$process_stat" | tail -n1 | tr -s ' ' | cut -d ' ' -f 4) 20 | echo "$process_stat" 21 | 22 | socket_stat=$(ss -uampn | grep $PROCESS -A1 -m 1 | tr -s ' ') 23 | recvQ=$(echo $socket_stat | head -1 | cut -d ' ' -f2) 24 | alloc=$(echo $socket_stat | tail -1 | sed 's/^.*(r\(.*\),rb.*/\1/') 25 | alloc_max=$(echo $socket_stat | tail -1 | sed 's/^.*,rb\(.*\),t0.*/\1/') 26 | drop=$(echo $socket_stat | tail -1 | sed 's/^.*,d\(.*\)).*/\1/') 27 | drop_per_sec=$(echo $drop/$uptime_sec | bc) 28 | 29 | show "Recv-Q" "$recvQ" "B" 30 | show "alloc" "$alloc" "B" 31 | show "alloc max" "$alloc_max" "B" 32 | show "drop" "$drop" "B" 33 | show "drop/s" "$drop_per_sec" "B/s" 34 | } 35 | 36 | while true; do 37 | sleep 1 38 | tput cup 0 0 39 | show_all 40 | done 41 | --------------------------------------------------------------------------------